From 931b4a0abaea1eac7aea5925641deb12fb8ed0f7 Mon Sep 17 00:00:00 2001 From: Thomas Peetz Date: Wed, 30 Apr 2025 17:31:18 +0200 Subject: [PATCH 1/2] remove obsolete kontor.py --- .gitignore | 16 + LICENSE | 9 + Makefile | 9 + docker-compose.yml | 44 + go/.env | 10 + go/.gitignore | 9 + go/.gitlab-ci.yml | 116 ++ go/Makefile | 73 + go/README.md | 4 + go/cmd/kontor/main.go | 36 + go/cmd/kontor/model.go | 25 + go/cmd/root.go | 89 ++ go/cmd/server.go | 32 + go/comics.xml | 63 + go/docs/build.gradle | 7 + go/docs/gradle.properties | 2 + go/docs/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + go/docs/gradlew | 240 ++++ go/docs/gradlew.bat | 91 ++ go/docs/settings.gradle | 1 + go/docs/src/docs/asciidoc/kontor-go.adoc | 31 + go/go.mod | 28 + go/go.sum | 62 + go/kontor-ansible.yml | 58 + go/pkg/admin/routes.go | 23 + go/pkg/admin/user.go | 18 + go/pkg/admin/user_dao.go | 123 ++ go/pkg/admin/user_test.go | 105 ++ go/pkg/admin/views.go | 242 ++++ go/pkg/application/registry/comic/registry.go | 19 + go/pkg/auth/middleware.go | 87 ++ go/pkg/auth/session.go | 11 + go/pkg/auth/session_dao.go | 78 ++ go/pkg/auth/session_test.go | 103 ++ go/pkg/comics/artist.go | 12 + go/pkg/comics/artist_dao.go | 78 ++ go/pkg/comics/artist_test.go | 96 ++ go/pkg/comics/artist_views.go | 23 + go/pkg/comics/comic.go | 15 + go/pkg/comics/comic_dao.go | 75 ++ go/pkg/comics/comic_test.go | 88 ++ go/pkg/comics/publisher.go | 12 + go/pkg/comics/publisher_dao.go | 75 ++ go/pkg/comics/publisher_test.go | 87 ++ go/pkg/comics/publisher_views.go | 70 + go/pkg/comics/routes.go | 28 + go/pkg/comics/views.go | 28 + go/pkg/context/comic/responses/comic.go | 15 + go/pkg/context/comic/routing/endpoints.go | 33 + go/pkg/dao/database.go | 31 + go/pkg/dao/database_test.go | 51 + go/pkg/infrastructure/app/factory.go | 50 + go/pkg/infrastructure/config/factory.go | 48 + go/pkg/infrastructure/kernel/app.go | 91 ++ go/pkg/infrastructure/response/response.go | 20 + go/pkg/library/author.go | 12 + go/pkg/library/author_dao.go | 74 + go/pkg/library/author_test.go | 99 ++ go/pkg/library/book.go | 18 + go/pkg/library/book_dao.go | 74 + go/pkg/library/book_test.go | 107 ++ go/pkg/library/publisher.go | 12 + go/pkg/library/publisher_dao.go | 74 + go/pkg/library/publisher_test.go | 103 ++ go/pkg/library/publisher_views.go | 68 + go/pkg/library/routes.go | 22 + go/pkg/library/views.go | 24 + go/pkg/office/routes.go | 13 + go/pkg/office/views.go | 11 + go/pkg/properties/root.go | 25 + go/pkg/setup/data.go | 20 + go/pkg/setup/routes.go | 38 + go/pkg/setup/session.go | 15 + go/pkg/setup/user.go | 20 + go/pkg/tradingcards/routes.go | 13 + go/pkg/tradingcards/views.go | 11 + go/pkg/tysc/card.go | 20 + go/pkg/tysc/card_dao.go | 66 + go/pkg/tysc/card_test.go | 110 ++ go/pkg/tysc/cardset.go | 13 + go/pkg/tysc/cardset_dao.go | 82 ++ go/pkg/tysc/cardset_test.go | 103 ++ go/pkg/tysc/insertset.go | 13 + go/pkg/tysc/insertset_dao.go | 82 ++ go/pkg/tysc/insertset_test.go | 103 ++ go/pkg/tysc/manufacturer.go | 12 + go/pkg/tysc/manufacturer_dao.go | 74 + go/pkg/tysc/manufacturer_test.go | 102 ++ go/pkg/tysc/parallelset.go | 13 + go/pkg/tysc/parallelset_dao.go | 82 ++ go/pkg/tysc/parallelset_test.go | 103 ++ go/pkg/tysc/player.go | 13 + go/pkg/tysc/player_dao.go | 74 + go/pkg/tysc/player_test.go | 103 ++ go/pkg/tysc/position.go | 14 + go/pkg/tysc/position_dao.go | 74 + go/pkg/tysc/position_test.go | 104 ++ go/pkg/tysc/routes.go | 22 + go/pkg/tysc/sport.go | 12 + go/pkg/tysc/sport_dao.go | 74 + go/pkg/tysc/sport_test.go | 102 ++ go/pkg/tysc/team.go | 14 + go/pkg/tysc/team_dao.go | 74 + go/pkg/tysc/team_test.go | 103 ++ go/pkg/tysc/views.go | 22 + go/pkg/util/render.go | 39 + go/sonar-project.properties | 2 + go/templates/comics/artists.html | 24 + go/templates/comics/comic.html | 34 + go/templates/comics/comics.html | 24 + go/templates/comics/menu.html | 38 + go/templates/comics/publisher.html | 32 + go/templates/comics/publishers.html | 25 + go/templates/kontor/admin-menu.html | 34 + go/templates/kontor/admin.html | 11 + go/templates/kontor/create-user.html | 40 + go/templates/kontor/data-upload.html | 29 + go/templates/kontor/footer.html | 26 + go/templates/kontor/header.html | 17 + go/templates/kontor/index.html | 17 + go/templates/kontor/login-successful.html | 12 + go/templates/kontor/login.html | 35 + go/templates/kontor/menu.html | 36 + go/templates/kontor/user-detail.html | 59 + go/templates/kontor/users.html | 38 + go/templates/library/authors.html | 25 + go/templates/library/books.html | 25 + go/templates/library/menu.html | 38 + go/templates/library/publisher.html | 32 + go/templates/library/publishers.html | 24 + go/templates/office/index.html | 10 + go/templates/office/menu.html | 38 + go/templates/tradingcards/index.html | 10 + go/templates/tradingcards/menu.html | 38 + go/templates/tysc/index.html | 16 + go/templates/tysc/menu.html | 38 + go/templates/tysc/sports.html | 28 + go/tysc-20041010-1819.sql | 168 +++ java-ee/ComicsImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++ .../config/checkstyle/checkstyle.xsl | 179 +++ .../ComicsImpl/config/findbugs/findbugs.xml | 15 + .../java/com/peetz/comics/dal/ArtistDao.java | 19 + .../java/com/peetz/comics/dal/ArtistImpl.java | 45 + .../java/com/peetz/comics/dal/ComicDao.java | 23 + .../java/com/peetz/comics/dal/ComicImpl.java | 62 + .../com/peetz/comics/entity/ArtistEntity.java | 69 + .../com/peetz/comics/entity/ComicEntity.java | 90 ++ .../com/peetz/comics/entity/IssueEntity.java | 80 ++ .../peetz/comics/entity/PublisherEntity.java | 48 + .../peetz/comics/entity/StoryArcEntity.java | 56 + .../com/peetz/comics/entity/VolumeEntity.java | 37 + .../peetz/comics/service/ComicService.java | 34 + .../comics/service/ComicServiceImpl.java | 92 ++ .../peetz/comics/service/package-info.java | 8 + .../comics/service/ComicServiceImplTest.java | 195 +++ java-ee/ComicsWeb/build.gradle | 7 + .../java/com/peetz/comics/view/ComicView.java | 36 + .../ComicsWeb/src/main/webapp/artistAdd.jsp | 64 + .../ComicsWeb/src/main/webapp/artistEdit.jsp | 66 + .../ComicsWeb/src/main/webapp/artistList.jsp | 95 ++ .../ComicsWeb/src/main/webapp/comicAdd.jsp | 67 + .../ComicsWeb/src/main/webapp/comicEdit.jsp | 90 ++ .../ComicsWeb/src/main/webapp/comicList.jsp | 99 ++ .../ComicsWeb/src/main/webapp/comics.xhtml | 51 + java-ee/ComicsWeb/src/main/webapp/index.jsp | 35 + .../ComicsWeb/src/main/webapp/issueAdd.jsp | 67 + .../ComicsWeb/src/main/webapp/issueEdit.jsp | 66 + .../src/main/webapp/publisherAdd.jsp | 64 + .../src/main/webapp/publisherEdit.jsp | 66 + .../src/main/webapp/publisherList.jsp | 95 ++ java-ee/DVDs.csv | 391 ++++++ java-ee/Jenkinsfile | 15 + java-ee/KontorApp/build.gradle | 19 + .../main/java/com/ibtp/kontor/KontorApp.java | 23 + .../main/java/com/ibtp/kontor/KontorGUI.java | 69 + .../com/ibtp/kontor/comics/dal/ArtistDao.java | 26 + .../ibtp/kontor/comics/dal/ArtistImpl.java | 67 + .../com/ibtp/kontor/comics/dal/ComicDao.java | 26 + .../com/ibtp/kontor/comics/dal/ComicImpl.java | 65 + .../com/ibtp/kontor/comics/dal/IssueDao.java | 23 + .../com/ibtp/kontor/comics/dal/IssueImpl.java | 57 + .../ibtp/kontor/comics/dal/PublisherDao.java | 26 + .../ibtp/kontor/comics/dal/PublisherImpl.java | 62 + .../ibtp/kontor/comics/dal/StoryArcDao.java | 24 + .../ibtp/kontor/comics/dal/StoryArcImpl.java | 55 + .../com/ibtp/kontor/comics/dal/VolumeDao.java | 24 + .../ibtp/kontor/comics/dal/VolumeImpl.java | 57 + .../kontor/comics/entity/ArtistEntity.java | 72 + .../kontor/comics/entity/ComicEntity.java | 81 ++ .../kontor/comics/entity/IssueEntity.java | 75 ++ .../kontor/comics/entity/PublisherEntity.java | 47 + .../kontor/comics/entity/StoryArcEntity.java | 48 + .../kontor/comics/entity/VolumeEntity.java | 40 + .../ibtp/kontor/comics/view/ComicsMenu.java | 13 + .../java/com/ibtp/kontor/dal/BaseImpl.java | 21 + .../java/com/ibtp/kontor/dal/Database.java | 11 + .../com/ibtp/kontor/dal/DatabaseManager.java | 23 + .../com/ibtp/kontor/dal/LocalDatabase.java | 103 ++ .../ibtp/kontor/library/dal/ArticleDao.java | 23 + .../ibtp/kontor/library/dal/ArticleImpl.java | 64 + .../ibtp/kontor/library/dal/AuthorDao.java | 25 + .../ibtp/kontor/library/dal/AuthorImpl.java | 65 + .../com/ibtp/kontor/library/dal/BookDao.java | 22 + .../com/ibtp/kontor/library/dal/BookImpl.java | 38 + .../com/ibtp/kontor/library/dal/FileDao.java | 22 + .../com/ibtp/kontor/library/dal/FileImpl.java | 38 + .../com/ibtp/kontor/library/dal/TitleDao.java | 22 + .../ibtp/kontor/library/dal/TitleImpl.java | 38 + .../kontor/library/entity/ArticleEntity.java | 56 + .../kontor/library/entity/AuthorEntity.java | 62 + .../kontor/library/entity/BookEntity.java | 96 ++ .../kontor/library/entity/FileEntity.java | 41 + .../kontor/library/entity/TitleEntity.java | 27 + .../ibtp/kontor/library/view/LibraryMenu.java | 13 + .../kontor/tradingcards/dal/BaseSetDao.java | 22 + .../kontor/tradingcards/dal/BaseSetImpl.java | 51 + .../kontor/tradingcards/dal/InsertDao.java | 22 + .../kontor/tradingcards/dal/InsertImpl.java | 38 + .../tradingcards/dal/ManufacturerDao.java | 29 + .../tradingcards/dal/ManufacturerImpl.java | 65 + .../tradingcards/dal/ParallelSetDao.java | 22 + .../tradingcards/dal/ParallelSetImpl.java | 38 + .../kontor/tradingcards/dal/PlayerDao.java | 24 + .../kontor/tradingcards/dal/PlayerImpl.java | 43 + .../kontor/tradingcards/dal/PositionDao.java | 26 + .../kontor/tradingcards/dal/PositionImpl.java | 64 + .../kontor/tradingcards/dal/SportCardDao.java | 22 + .../tradingcards/dal/SportCardImpl.java | 38 + .../kontor/tradingcards/dal/SportDao.java | 26 + .../kontor/tradingcards/dal/SportImpl.java | 64 + .../ibtp/kontor/tradingcards/dal/TeamDao.java | 26 + .../kontor/tradingcards/dal/TeamImpl.java | 66 + .../tradingcards/entity/BaseSetEntity.java | 67 + .../tradingcards/entity/InsertEntity.java | 74 + .../entity/ManufacturerEntity.java | 78 ++ .../entity/ParallelSetEntity.java | 53 + .../tradingcards/entity/PlayerEntity.java | 46 + .../tradingcards/entity/PositionEntity.java | 56 + .../tradingcards/entity/SportCardEntity.java | 68 + .../tradingcards/entity/SportEntity.java | 75 ++ .../tradingcards/entity/TeamEntity.java | 48 + .../tradingcards/view/TradingCardsMenu.java | 13 + .../main/resources/META-INF/persistence.xml | 39 + .../KontorApp/src/main/resources/logback.xml | 40 + .../ibtp/kontor/comics/CollectionTest.java | 41 + .../kontor/comics/dal/ArtistImplTest.java | 50 + .../ibtp/kontor/comics/dal/ComicImplTest.java | 54 + .../ibtp/kontor/comics/dal/IssueImplTest.java | 62 + .../kontor/comics/dal/PublisherImplTest.java | 50 + .../kontor/comics/dal/StoryArcImplTest.java | 55 + .../kontor/comics/dal/VolumeImplTest.java | 67 + .../ibtp/kontor/dal/DataAccessLayerTest.java | 62 + .../ibtp/kontor/library/BookshelfTest.java | 22 + .../kontor/library/dal/ArticleImplTest.java | 41 + .../kontor/library/dal/AuthorImplTest.java | 52 + .../ibtp/kontor/library/dal/BookImplTest.java | 27 + .../ibtp/kontor/library/dal/FileImplTest.java | 27 + .../kontor/library/dal/TitleImplTest.java | 27 + .../kontor/tradingcards/CollectionTest.java | 167 +++ .../tradingcards/dal/BaseSetImplTest.java | 27 + .../tradingcards/dal/InsertImplTest.java | 28 + .../dal/ManufacturerImplTest.java | 52 + .../tradingcards/dal/ParallelSetImplTest.java | 27 + .../tradingcards/dal/PlayerImplTest.java | 27 + .../tradingcards/dal/PositionImplTest.java | 42 + .../tradingcards/dal/SportCardImplTest.java | 27 + .../tradingcards/dal/SportImplTest.java | 41 + .../kontor/tradingcards/dal/TeamImplTest.java | 41 + .../ibtp/kontor/util/LocalTestDatabase.java | 110 ++ .../test/resources/META-INF/persistence.xml | 39 + .../KontorApp/src/test/resources/logback.xml | 41 + java-ee/KontorEJB/build.gradle | 12 + .../java/com/ibtp/kontor/ejb/Controller.java | 62 + .../java/com/ibtp/kontor/ejb/Property.java | 37 + .../com/ibtp/kontor/ejb/PropertyManager.java | 28 + java-ee/KontorImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++ .../config/checkstyle/checkstyle.xsl | 179 +++ .../KontorImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/kontor/dal/KontorUserDao.java | 16 + .../com/peetz/kontor/dal/KontorUserImpl.java | 53 + .../peetz/kontor/entity/KontorUserEntity.java | 62 + .../peetz/kontor/service/package-info.java | 8 + java-ee/KontorWeb/build.gradle | 11 + .../com/peetz/kontor/data/ExportComics.java | 88 ++ .../com/peetz/kontor/data/ExportLibrary.java | 100 ++ .../com/peetz/kontor/data/ExportMedien.java | 75 ++ .../peetz/kontor/data/ExportTradingCards.java | 175 +++ .../com/peetz/kontor/data/FileExport.java | 64 + .../com/peetz/kontor/data/FileImport.java | 72 + .../com/peetz/kontor/data/ImportComics.java | 81 ++ .../com/peetz/kontor/data/ImportLibrary.java | 88 ++ .../com/peetz/kontor/data/ImportMedien.java | 91 ++ .../peetz/kontor/data/ImportTradingCards.java | 52 + .../main/resources/META-INF/persistence.xml | 51 + .../src/main/webapp/WEB-INF/faces-config.xml | 77 ++ .../src/main/webapp/WEB-INF/glassfish-web.xml | 7 + .../KontorWeb/src/main/webapp/WEB-INF/web.xml | 22 + .../KontorWeb/src/main/webapp/comics.xhtml | 39 + .../KontorWeb/src/main/webapp/css/store.css | 145 ++ java-ee/KontorWeb/src/main/webapp/index.xhtml | 48 + .../src/main/webapp/kontorTemplate.xhtml | 44 + .../KontorWeb/src/main/webapp/library.xhtml | 39 + .../KontorWeb/src/main/webapp/medien.xhtml | 39 + .../main/webapp/resources/css/cssLayout.css | 71 + .../src/main/webapp/resources/css/default.css | 29 + java-ee/KontorWeb/src/main/webapp/seite.html | 17 + java-ee/KontorWeb/src/main/webapp/sport.xhtml | 40 + .../src/main/webapp/sport/sportAdd.xhtml | 39 + .../src/main/webapp/sport/sportDetails.xhtml | 39 + .../src/main/webapp/tradingcards.xhtml | 40 + java-ee/LibraryImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++ .../config/checkstyle/checkstyle.xsl | 179 +++ .../LibraryImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/library/dal/ArticleDao.java | 25 + .../java/com/peetz/library/dal/BookDao.java | 15 + .../com/peetz/library/dal/BookshelfDao.java | 22 + .../java/com/peetz/library/dal/FileDao.java | 15 + .../com/peetz/library/dal/MagazineDao.java | 15 + .../com/peetz/library/dal/ShelfObjectDao.java | 15 + .../com/peetz/library/dal/ShelfboardDao.java | 15 + .../com/peetz/library/dal/package-info.java | 8 + .../peetz/library/entity/ArticleEntity.java | 87 ++ .../com/peetz/library/entity/BookEntity.java | 93 ++ .../peetz/library/entity/BookshelfEntity.java | 53 + .../com/peetz/library/entity/FileEntity.java | 49 + .../peetz/library/entity/MagazineEntity.java | 49 + .../library/entity/ShelfObjectEntity.java | 32 + .../library/entity/ShelfboardEntity.java | 57 + .../peetz/library/entity/package-info.java | 8 + .../peetz/library/service/LibraryService.java | 28 + .../library/service/LibraryServiceImpl.java | 64 + .../peetz/library/service/package-info.java | 8 + java-ee/LibraryWeb/build.gradle | 7 + .../com/peetz/library/view/LibraryView.java | 42 + java-ee/LibraryWeb/src/main/webapp/index.jsp | 33 + .../src/main/webapp/jsp/articleAdd.jsp | 67 + .../src/main/webapp/jsp/articleEdit.jsp | 69 + .../src/main/webapp/jsp/articleList.jsp | 90 ++ .../src/main/webapp/jsp/boardAdd.jsp | 68 + .../src/main/webapp/jsp/boardEdit.jsp | 67 + .../src/main/webapp/jsp/bookAdd.jsp | 67 + .../src/main/webapp/jsp/bookEdit.jsp | 85 ++ .../src/main/webapp/jsp/bookList.jsp | 33 + .../LibraryWeb/src/main/webapp/jsp/index.jsp | 58 + .../src/main/webapp/jsp/shelfAdd.jsp | 67 + .../src/main/webapp/jsp/shelfEdit.jsp | 92 ++ .../src/main/webapp/jsp/shelfList.jsp | 91 ++ java-ee/MedienImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++ .../config/checkstyle/checkstyle.xsl | 179 +++ .../MedienImpl/config/findbugs/findbugs.xml | 15 + .../com/peetz/medien/dal/package-info.java | 8 + .../peetz/medien/entity/AudioCDEntity.java | 91 ++ .../com/peetz/medien/entity/BoxSetEntity.java | 43 + .../com/peetz/medien/entity/FilmEntity.java | 50 + .../com/peetz/medien/entity/package-info.java | 8 + .../peetz/medien/service/MedienService.java | 22 + .../medien/service/MedienServiceImpl.java | 75 ++ .../peetz/medien/service/package-info.java | 8 + java-ee/MedienWeb/build.gradle | 7 + .../com/peetz/medien/view/MedienView.java | 36 + java-ee/MedienWeb/src/main/webapp/index.jsp | 33 + .../MedienWeb/src/main/webapp/jsp/cdAdd.jsp | 75 ++ .../MedienWeb/src/main/webapp/jsp/cdEdit.jsp | 75 ++ .../MedienWeb/src/main/webapp/jsp/cdList.jsp | 94 ++ .../MedienWeb/src/main/webapp/jsp/dvdAdd.jsp | 67 + .../MedienWeb/src/main/webapp/jsp/dvdEdit.jsp | 69 + .../MedienWeb/src/main/webapp/jsp/dvdList.jsp | 90 ++ .../MedienWeb/src/main/webapp/jsp/index.jsp | 58 + java-ee/README.md | 2 + java-ee/TradingCardsImpl/build.gradle | 5 + .../config/checkstyle/checkstyle.xml | 192 +++ .../config/checkstyle/checkstyle.xsl | 179 +++ .../config/findbugs/findbugs.xml | 15 + .../tradingcards/dal/ManufacturerDao.java | 31 + .../tradingcards/dal/ManufacturerImpl.java | 67 + .../com/peetz/tradingcards/dal/SportDao.java | 28 + .../com/peetz/tradingcards/dal/SportImpl.java | 58 + .../tradingcards/entity/BaseSetEntity.java | 67 + .../tradingcards/entity/InsertEntity.java | 74 + .../entity/ManufacturerEntity.java | 74 + .../entity/ParallelSetEntity.java | 53 + .../tradingcards/entity/PlayerEntity.java | 46 + .../tradingcards/entity/PositionEntity.java | 53 + .../tradingcards/entity/SportCardEntity.java | 68 + .../tradingcards/entity/SportEntity.java | 64 + .../peetz/tradingcards/entity/TeamEntity.java | 42 + .../tradingcards/service/SportService.java | 24 + .../service/SportServiceImpl.java | 63 + .../service/TradingcardService.java | 28 + .../service/TradingcardServiceImpl.java | 75 ++ .../tradingcards/service/package-info.java | 5 + .../dal/ManufacturerImplTest.java | 147 ++ java-ee/TradingCardsWeb/build.gradle | 7 + .../peetz/tradingcards/view/SportView.java | 52 + .../tradingcards/view/TradingCardsView.java | 36 + .../TradingCardsWeb/src/main/webapp/index.jsp | 33 + java-ee/build.gradle | 69 + java-ee/comics.xml | 63 + java-ee/config/checkstyle/checkstyle.xml | 192 +++ java-ee/config/checkstyle/checkstyle.xsl | 179 +++ java-ee/config/findbugs/findbugs.xml | 15 + java-ee/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54208 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + java-ee/gradlew | 172 +++ java-ee/gradlew.bat | 84 ++ java-ee/settings.gradle | 13 + java-quarkus/.gitignore | 9 + java-quarkus/.gitlab-ci.yml | 38 + java-quarkus/README.md | 60 + java-quarkus/build.gradle | 158 +++ java-quarkus/config/checkstyle/checkstyle.xml | 14 + java-quarkus/gradle.properties | 9 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + java-quarkus/gradlew | 185 +++ java-quarkus/gradlew.bat | 104 ++ java-quarkus/settings.gradle | 11 + .../src/docs/asciidoc/kontor-quarkus.adoc | 84 ++ java-quarkus/src/main/docker/Dockerfile.jvm | 94 ++ .../src/main/docker/Dockerfile.legacy-jar | 90 ++ .../src/main/docker/Dockerfile.native | 27 + .../src/main/docker/Dockerfile.native-micro | 30 + .../thpeetz/kontor/comics/ComicsResource.java | 95 ++ .../kontor/comics/service/ArtistService.java | 28 + .../kontor/comics/service/ComicsService.java | 12 + .../comics/service/PublisherService.java | 28 + .../resources/META-INF/resources/index.html | 284 ++++ .../src/main/resources/application.properties | 3 + .../kontor/comics/ComicsResourceIT.java | 8 + .../kontor/comics/ComicsResourceTest.java | 19 + java/README.md | 2 + java/build.gradle | 87 ++ java/comics.xml | 63 + java/config/checkstyle/checkstyle.xml | 192 +++ java/config/checkstyle/checkstyle.xsl | 179 +++ java/config/findbugs/findbugs.xml | 15 + java/gradle.properties | 2 + java/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes java/gradle/wrapper/gradle-wrapper.properties | 5 + java/gradlew | 185 +++ java/gradlew.bat | 89 ++ java/settings.gradle | 1 + .../main/java/com/ibtp/kontor/Database.java | 46 + .../main/java/com/ibtp/kontor/DumpComics.java | 51 + .../main/java/com/ibtp/kontor/KontorApp.java | 23 + .../main/java/com/ibtp/kontor/KontorGUI.java | 69 + .../com/ibtp/kontor/comics/dal/ArtistDao.java | 26 + .../ibtp/kontor/comics/dal/ArtistImpl.java | 68 + .../com/ibtp/kontor/comics/dal/ComicDao.java | 26 + .../com/ibtp/kontor/comics/dal/ComicImpl.java | 65 + .../com/ibtp/kontor/comics/dal/IssueDao.java | 23 + .../com/ibtp/kontor/comics/dal/IssueImpl.java | 57 + .../ibtp/kontor/comics/dal/PublisherDao.java | 26 + .../ibtp/kontor/comics/dal/PublisherImpl.java | 62 + .../ibtp/kontor/comics/dal/StoryArcDao.java | 24 + .../ibtp/kontor/comics/dal/StoryArcImpl.java | 55 + .../com/ibtp/kontor/comics/dal/VolumeDao.java | 24 + .../ibtp/kontor/comics/dal/VolumeImpl.java | 57 + .../com/ibtp/kontor/comics/entity/Artist.java | 46 + .../kontor/comics/entity/ArtistEntity.java | 72 + .../com/ibtp/kontor/comics/entity/Comic.java | 98 ++ .../kontor/comics/entity/ComicEntity.java | 81 ++ .../com/ibtp/kontor/comics/entity/Issue.java | 66 + .../kontor/comics/entity/IssueEntity.java | 75 ++ .../ibtp/kontor/comics/entity/Publisher.java | 60 + .../kontor/comics/entity/PublisherEntity.java | 47 + .../ibtp/kontor/comics/entity/StoryArc.java | 59 + .../kontor/comics/entity/StoryArcEntity.java | 48 + .../kontor/comics/entity/TradePaperback.java | 56 + .../com/ibtp/kontor/comics/entity/Volume.java | 70 + .../kontor/comics/entity/VolumeEntity.java | 40 + .../ibtp/kontor/comics/view/ComicsMenu.java | 13 + .../java/com/ibtp/kontor/dal/BaseImpl.java | 21 + .../java/com/ibtp/kontor/dal/Database.java | 11 + .../com/ibtp/kontor/dal/DatabaseManager.java | 23 + .../com/ibtp/kontor/dal/LocalDatabase.java | 104 ++ .../ibtp/kontor/library/dal/ArticleDao.java | 23 + .../ibtp/kontor/library/dal/ArticleImpl.java | 64 + .../ibtp/kontor/library/dal/AuthorDao.java | 25 + .../ibtp/kontor/library/dal/AuthorImpl.java | 65 + .../com/ibtp/kontor/library/dal/BookDao.java | 22 + .../com/ibtp/kontor/library/dal/BookImpl.java | 38 + .../com/ibtp/kontor/library/dal/FileDao.java | 22 + .../com/ibtp/kontor/library/dal/FileImpl.java | 38 + .../com/ibtp/kontor/library/dal/TitleDao.java | 22 + .../ibtp/kontor/library/dal/TitleImpl.java | 38 + .../kontor/library/entity/ArticleEntity.java | 56 + .../kontor/library/entity/AuthorEntity.java | 62 + .../kontor/library/entity/BookEntity.java | 96 ++ .../kontor/library/entity/FileEntity.java | 41 + .../kontor/library/entity/TitleEntity.java | 27 + .../ibtp/kontor/library/view/LibraryMenu.java | 13 + .../kontor/tradingcards/dal/BaseSetDao.java | 22 + .../kontor/tradingcards/dal/BaseSetImpl.java | 51 + .../kontor/tradingcards/dal/InsertDao.java | 22 + .../kontor/tradingcards/dal/InsertImpl.java | 38 + .../tradingcards/dal/ManufacturerDao.java | 29 + .../tradingcards/dal/ManufacturerImpl.java | 65 + .../tradingcards/dal/ParallelSetDao.java | 22 + .../tradingcards/dal/ParallelSetImpl.java | 38 + .../kontor/tradingcards/dal/PlayerDao.java | 24 + .../kontor/tradingcards/dal/PlayerImpl.java | 43 + .../kontor/tradingcards/dal/PositionDao.java | 26 + .../kontor/tradingcards/dal/PositionImpl.java | 65 + .../kontor/tradingcards/dal/SportCardDao.java | 22 + .../tradingcards/dal/SportCardImpl.java | 38 + .../kontor/tradingcards/dal/SportDao.java | 26 + .../kontor/tradingcards/dal/SportImpl.java | 64 + .../ibtp/kontor/tradingcards/dal/TeamDao.java | 26 + .../kontor/tradingcards/dal/TeamImpl.java | 66 + .../tradingcards/entity/BaseSetEntity.java | 67 + .../tradingcards/entity/InsertEntity.java | 74 + .../entity/ManufacturerEntity.java | 78 ++ .../entity/ParallelSetEntity.java | 53 + .../tradingcards/entity/PlayerEntity.java | 46 + .../tradingcards/entity/PositionEntity.java | 66 + .../tradingcards/entity/SportCardEntity.java | 68 + .../tradingcards/entity/SportEntity.java | 75 ++ .../tradingcards/entity/TeamEntity.java | 48 + .../tradingcards/view/TradingCardsMenu.java | 13 + .../main/resources/META-INF/persistence.xml | 39 + java/src/main/resources/logback.xml | 40 + .../ibtp/kontor/comics/CollectionTest.java | 43 + .../kontor/comics/dal/ArtistImplTest.java | 65 + .../ibtp/kontor/comics/dal/ComicImplTest.java | 57 + .../ibtp/kontor/comics/dal/IssueImplTest.java | 64 + .../kontor/comics/dal/PublisherImplTest.java | 56 + .../kontor/comics/dal/StoryArcImplTest.java | 63 + .../kontor/comics/dal/VolumeImplTest.java | 72 + .../ibtp/kontor/dal/DataAccessLayerTest.java | 63 + .../ibtp/kontor/library/BookshelfTest.java | 23 + .../kontor/library/dal/ArticleImplTest.java | 57 + .../kontor/library/dal/AuthorImplTest.java | 56 + .../ibtp/kontor/library/dal/BookImplTest.java | 28 + .../ibtp/kontor/library/dal/FileImplTest.java | 28 + .../kontor/library/dal/TitleImplTest.java | 28 + .../kontor/tradingcards/CollectionTest.java | 168 +++ .../tradingcards/dal/BaseSetImplTest.java | 28 + .../tradingcards/dal/InsertImplTest.java | 29 + .../dal/ManufacturerImplTest.java | 57 + .../tradingcards/dal/ParallelSetImplTest.java | 28 + .../tradingcards/dal/PlayerImplTest.java | 28 + .../tradingcards/dal/PositionImplTest.java | 44 + .../tradingcards/dal/SportCardImplTest.java | 28 + .../tradingcards/dal/SportImplTest.java | 44 + .../kontor/tradingcards/dal/TeamImplTest.java | 44 + .../ibtp/kontor/util/LocalTestDatabase.java | 110 ++ .../test/resources/META-INF/persistence.xml | 39 + java/src/test/resources/logback.xml | 41 + java/tysc-20041010-1819.sql | 168 +++ kontor-api/.coverage | Bin 0 -> 53248 bytes kontor-api/.gitignore | 1 + kontor-api/.python-version | 1 + kontor-api/Dockerfile | 55 + kontor-api/Makefile | 14 + kontor-api/README.md | 0 kontor-api/pyproject.toml | 25 + kontor-api/src/__init__.py | 0 kontor-api/src/apis/__init__.py | 0 kontor-api/src/apis/base.py | 8 + kontor-api/src/apis/utils.py | 8 + kontor-api/src/apis/version1/__init__.py | 0 kontor-api/src/apis/version1/comic.py | 62 + kontor-api/src/apis/version1/media.py | 76 ++ kontor-api/src/apis/version1/tysc.py | 20 + kontor-api/src/core/__init__.py | 0 kontor-api/src/core/config.py | 23 + kontor-api/src/db/__init__.py | 0 kontor-api/src/db/models/__init__.py | 0 kontor-api/src/db/models/admin.py | 78 ++ kontor-api/src/db/models/base.py | 31 + kontor-api/src/db/models/bookshelf.py | 50 + kontor-api/src/db/models/comic.py | 128 ++ kontor-api/src/db/models/database.py | 396 ++++++ kontor-api/src/db/models/media.py | 101 ++ kontor-api/src/db/models/metadata.py | 42 + kontor-api/src/db/models/tysc.py | 100 ++ kontor-api/src/db/session.py | 15 + kontor-api/src/main.py | 29 + kontor-api/src/schema/__init__.py | 0 kontor-api/src/schema/comics/__init__.py | 0 kontor-api/src/schema/comics/artist.py | 36 + kontor-api/src/schema/comics/comic.py | 56 + kontor-api/src/schema/media/__init__.py | 0 kontor-api/src/schema/media/file.py | 45 + kontor-api/src/schema/tysc/__init__.py | 0 kontor-api/src/schema/tysc/sport.py | 8 + kontor-api/src/static/images/cross.png | Bin 0 -> 544 bytes kontor-api/src/static/images/logo.png | Bin 0 -> 17196 bytes kontor-api/src/static/images/tick.png | Bin 0 -> 634 bytes kontor-api/src/static/js/autocomplete.js | 5 + .../src/templates/comic/artist_detail.html | 50 + kontor-api/src/templates/comic/artists.html | 32 + .../src/templates/comic/comic_detail.html | 71 + kontor-api/src/templates/comic/comics.html | 31 + .../src/templates/comic/publisher_detail.html | 45 + .../src/templates/comic/publishers.html | 31 + .../src/templates/components/actor_cards.html | 6 + .../src/templates/components/alerts.html | 5 + .../templates/components/artist_cards.html | 6 + .../src/templates/components/check.html | 5 + .../src/templates/components/comic_cards.html | 8 + .../src/templates/components/navbar.html | 66 + .../templates/components/publisher_cards.html | 6 + kontor-api/src/templates/index.html | 14 + .../src/templates/media/actor_detail.html | 45 + kontor-api/src/templates/media/actors.html | 32 + .../src/templates/media/file_detail.html | 65 + kontor-api/src/templates/media/files.html | 29 + kontor-api/src/templates/shared/base.html | 26 + kontor-api/src/webapps/__init__.py | 0 kontor-api/src/webapps/base.py | 15 + kontor-api/src/webapps/comic/__init__.py | 0 kontor-api/src/webapps/comic/route_comics.py | 42 + kontor-api/src/webapps/media/route_media.py | 32 + kontor-api/tests/__init__.py | 0 kontor-api/tests/test_main.py | 15 + kontor-api/uv.lock | 811 +++++++++++ kontor-gui/.gitignore | 9 + kontor-gui/Makefile | 31 + kontor-gui/README.md | 1 + kontor-gui/gui/__init__.py | 0 kontor-gui/gui/comic_window.py | 66 + kontor-gui/gui/data_view.py | 12 + kontor-gui/gui/data_view_model.py | 32 + kontor-gui/gui/dialogs.py | 106 ++ kontor-gui/gui/main_window.py | 237 ++++ kontor-gui/gui/media_window.py | 77 ++ kontor-gui/gui/meta_data_window.py | 66 + kontor-gui/gui/model_config.py | 55 + kontor-gui/gui/progress.py | 18 + kontor-gui/gui/table_details.py | 60 + kontor-gui/gui/table_model.py | 128 ++ kontor-gui/gui/worker.py | 27 + kontor-gui/main.py | 48 + kontor-gui/pyproject.toml | 26 + kontor-gui/pysidedeploy.spec | 98 ++ kontor-gui/pyvenv.cfg | 5 + kontor-gui/requirements.txt | 6 + kontor-gui/res/application-export.png | Bin 0 -> 513 bytes kontor-gui/res/application-import.png | Bin 0 -> 524 bytes kontor-gui/res/arrow-circle-double.png | Bin 0 -> 836 bytes kontor-gui/res/cross.png | Bin 0 -> 544 bytes kontor-gui/res/tick.png | Bin 0 -> 634 bytes kontor-gui/src/kontor_gui/__init__.py | 0 kontor-gui/tests/__init__.py | 0 kontor-schema/.gitignore | 1 + kontor-schema/.python-version | 1 + kontor-schema/README.md | 0 kontor-schema/pyproject.toml | 18 + kontor-schema/src/kontor_schema/__init__.py | 10 + kontor-schema/src/kontor_schema/admin.py | 82 ++ kontor-schema/src/kontor_schema/base.py | 31 + kontor-schema/src/kontor_schema/bookshelf.py | 50 + kontor-schema/src/kontor_schema/comic.py | 100 ++ kontor-schema/src/kontor_schema/database.py | 396 ++++++ kontor-schema/src/kontor_schema/media.py | 93 ++ kontor-schema/src/kontor_schema/metadata.py | 42 + kontor-schema/src/kontor_schema/tysc.py | 100 ++ kontor-schema/uv.lock | 161 +++ kontor-scripts/.python-version | 1 + kontor-scripts/Makefile | 15 + kontor-scripts/README.md | 3 + kontor-scripts/check_kontor.py | 183 +++ kontor-scripts/config.py | 118 ++ kontor-scripts/copy_to_mariadb.py | 52 + kontor-scripts/copy_to_sqlite.py | 51 + kontor-scripts/db_structure.py | 67 + kontor-scripts/download.py | 122 ++ kontor-scripts/export.py | 43 + kontor-scripts/import.py | 42 + kontor-scripts/json_to_mariadb.py | 58 + kontor-scripts/pyproject.toml | 21 + kontor-scripts/read_list.py | 57 + kontor-scripts/requirements.txt | 10 + kontor-scripts/schema/__init__.py | 0 kontor-scripts/schema/admin.py | 78 ++ kontor-scripts/schema/base.py | 31 + kontor-scripts/schema/bookshelf.py | 50 + kontor-scripts/schema/comic.py | 100 ++ kontor-scripts/schema/database.py | 399 ++++++ kontor-scripts/schema/media.py | 99 ++ kontor-scripts/schema/metadata.py | 42 + kontor-scripts/schema/tysc.py | 100 ++ kontor-scripts/update_title.py | 59 + kontor-scripts/uv.lock | 699 ++++++++++ kontor-spring/.factorypath | 3 + kontor-spring/.gitattributes | 9 + kontor-spring/.gitignore | 33 + kontor-spring/Dockerfile | 5 + kontor-spring/Makefile | 8 + kontor-spring/README.md | 3 + kontor-spring/build.gradle | 250 ++++ .../frontend/themes/kontor/styles.css | 10 + .../frontend/themes/kontor/theme.json | 3 + kontor-spring/gradle.properties | 3 + kontor-spring/gradle/libs.versions.toml | 66 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + kontor-spring/gradlew | 249 ++++ kontor-spring/gradlew.bat | 92 ++ kontor-spring/settings.gradle | 24 + .../src/docs/asciidoc/kontor-spring.adoc | 509 +++++++ .../kontor/comics/views/ArtistViewTest.java | 41 + .../kontor/comics/views/ArtistformTest.java | 63 + .../kontor/comics/views/ComicViewTest.java | 43 + .../comics/views/ComicWorkViewTest.java | 43 + .../kontor/comics/views/IssueViewTest.java | 43 + .../comics/views/PublisherViewTest.java | 41 + .../kontor/comics/views/StoryArcViewTest.java | 43 + .../comics/views/TradePaperbackViewTest.java | 47 + .../kontor/comics/views/VolumeViewTest.java | 50 + .../kontor/comics/views/WorktypeViewTest.java | 49 + .../kontor/tysc/views/CardSetViewTest.java | 43 + .../kontor/tysc/views/CardViewTest.java | 43 + .../tysc/views/FieldPositionViewTest.java | 43 + .../kontor/tysc/views/PlayerViewTest.java | 44 + .../kontor/tysc/views/RoosterViewTest.java | 43 + .../kontor/tysc/views/SportViewTest.java | 43 + .../kontor/tysc/views/TeamViewTest.java | 43 + .../kontor/tysc/views/VendorViewTest.java | 43 + .../resources/application.properties | 30 + .../java/de/thpeetz/kontor/Application.java | 24 + .../thpeetz/kontor/admin/AdminConstants.java | 56 + .../thpeetz/kontor/admin/MailProperties.java | 78 ++ .../kontor/admin/SetupModuleAdmin.java | 434 ++++++ .../thpeetz/kontor/admin/data/Assignment.java | 34 + .../kontor/admin/data/MetaDataColumn.java | 69 + .../kontor/admin/data/MetaDataTable.java | 40 + .../thpeetz/kontor/admin/data/ModuleData.java | 25 + .../thpeetz/kontor/admin/data/Permission.java | 29 + .../de/thpeetz/kontor/admin/data/Profile.java | 57 + .../de/thpeetz/kontor/admin/data/Token.java | 37 + .../repository/AssignmentRepository.java | 13 + .../repository/MailAccountRepository.java | 8 + .../repository/MetaDataColumnRepository.java | 18 + .../repository/MetaDataTableRepository.java | 9 + .../repository/ModuleDataRepository.java | 16 + .../repository/PermissionRepository.java | 19 + .../admin/repository/ProfileRepository.java | 18 + .../kontor/admin/services/AdminService.java | 107 ++ .../services/KontorUserDetailsService.java | 129 ++ .../kontor/admin/services/MailService.java | 24 + .../admin/services/MetaDataService.java | 243 ++++ .../kontor/admin/services/ModuleService.java | 76 ++ .../kontor/admin/views/AdminLayout.java | 36 + .../kontor/admin/views/AssignmentForm.java | 112 ++ .../kontor/admin/views/AssignmentView.java | 114 ++ .../thpeetz/kontor/admin/views/LoginView.java | 51 + .../kontor/admin/views/MetaDataForm.java | 123 ++ .../kontor/admin/views/MetaDataView.java | 193 +++ .../kontor/admin/views/ModuleDataForm.java | 100 ++ .../kontor/admin/views/ModuleDataView.java | 140 ++ .../kontor/admin/views/PermissionForm.java | 101 ++ .../kontor/admin/views/PermissionView.java | 119 ++ .../kontor/admin/views/ProfileForm.java | 143 ++ .../kontor/admin/views/ProfileView.java | 135 ++ .../kontor/admin/views/UserProfileView.java | 73 + .../kontor/bookshelf/BookshelfConstants.java | 53 + .../bookshelf/SetupModuleBookshelf.java | 82 ++ .../kontor/bookshelf/data/Article.java | 27 + .../kontor/bookshelf/data/ArticleAuthor.java | 29 + .../data/ArticleAuthorRepository.java | 12 + .../bookshelf/data/ArticleRepository.java | 16 + .../thpeetz/kontor/bookshelf/data/Author.java | 44 + .../bookshelf/data/AuthorRepository.java | 18 + .../thpeetz/kontor/bookshelf/data/Book.java | 42 + .../kontor/bookshelf/data/BookAuthor.java | 29 + .../bookshelf/data/BookAuthorRepository.java | 12 + .../kontor/bookshelf/data/BookRepository.java | 21 + .../bookshelf/data/BookshelfPublisher.java | 32 + .../data/BookshelfPublisherRepository.java | 18 + .../bookshelf/services/BookshelfService.java | 118 ++ .../kontor/bookshelf/views/ArticleForm.java | 106 ++ .../kontor/bookshelf/views/ArticleView.java | 126 ++ .../kontor/bookshelf/views/AuthorForm.java | 117 ++ .../kontor/bookshelf/views/AuthorView.java | 132 ++ .../kontor/bookshelf/views/BookForm.java | 111 ++ .../kontor/bookshelf/views/BookView.java | 124 ++ .../bookshelf/views/BookshelfLayout.java | 34 + .../views/BookshelfPublisherView.java | 131 ++ .../kontor/bookshelf/views/PublisherForm.java | 108 ++ .../thpeetz/kontor/comics/ComicConstants.java | 93 ++ .../kontor/comics/SetupModuleComics.java | 1198 +++++++++++++++++ .../de/thpeetz/kontor/comics/data/Artist.java | 45 + .../kontor/comics/data/ArtistRepository.java | 17 + .../de/thpeetz/kontor/comics/data/Comic.java | 81 ++ .../kontor/comics/data/ComicRepository.java | 19 + .../thpeetz/kontor/comics/data/ComicWork.java | 48 + .../comics/data/ComicWorkRepository.java | 15 + .../de/thpeetz/kontor/comics/data/Issue.java | 46 + .../kontor/comics/data/IssueRepository.java | 17 + .../thpeetz/kontor/comics/data/Publisher.java | 40 + .../comics/data/PublisherRepository.java | 17 + .../thpeetz/kontor/comics/data/StoryArc.java | 31 + .../comics/data/StoryArcRepository.java | 17 + .../kontor/comics/data/TradePaperback.java | 32 + .../comics/data/TradePaperbackRepository.java | 20 + .../de/thpeetz/kontor/comics/data/Volume.java | 42 + .../kontor/comics/data/VolumeRepository.java | 13 + .../thpeetz/kontor/comics/data/Worktype.java | 44 + .../comics/data/WorktypeRepository.java | 18 + .../kontor/comics/services/ComicService.java | 299 ++++ .../kontor/comics/views/ArtistForm.java | 117 ++ .../kontor/comics/views/ArtistView.java | 124 ++ .../kontor/comics/views/ComicForm.java | 130 ++ .../kontor/comics/views/ComicLayout.java | 43 + .../kontor/comics/views/ComicView.java | 161 +++ .../kontor/comics/views/ComicWorkForm.java | 113 ++ .../kontor/comics/views/ComicWorkView.java | 122 ++ .../kontor/comics/views/IssueForm.java | 113 ++ .../kontor/comics/views/IssueView.java | 169 +++ .../kontor/comics/views/PublisherForm.java | 100 ++ .../kontor/comics/views/PublisherView.java | 130 ++ .../kontor/comics/views/StoryArcForm.java | 108 ++ .../kontor/comics/views/StoryArcView.java | 130 ++ .../comics/views/TradePaperBackForm.java | 109 ++ .../comics/views/TradePaperbackView.java | 129 ++ .../kontor/comics/views/VolumeForm.java | 109 ++ .../kontor/comics/views/VolumeView.java | 130 ++ .../kontor/comics/views/WorktypeForm.java | 121 ++ .../kontor/comics/views/WorktypeView.java | 130 ++ .../kontor/common/data/AbstractEntity.java | 45 + .../common/data/AbstractLinkEntity.java | 55 + .../kontor/common/views/AvatarMenuBar.java | 52 + .../common/views/ColumnToggleContextMenu.java | 32 + .../kontor/common/views/KontorLayoutUtil.java | 99 ++ .../kontor/common/views/MainLayout.java | 102 ++ .../thpeetz/kontor/common/views/MainView.java | 18 + .../common/views/SeparateMainLayout.java | 31 + .../kontor/common/views/StatusIcon.java | 19 + .../data/services/DataManagementService.java | 46 + .../kontor/data/views/DataManagementView.java | 49 + .../thpeetz/kontor/data/views/ImportArea.java | 111 ++ .../thpeetz/kontor/data/views/UploadArea.java | 58 + .../thpeetz/kontor/mailclient/data/Mail.java | 20 + .../kontor/mailclient/data/MailAccount.java | 29 + .../kontor/mailclient/views/EmailView.java | 222 +++ .../thpeetz/kontor/media/MediaConstants.java | 41 + .../kontor/media/SetupModuleMedia.java | 39 + .../thpeetz/kontor/media/data/MediaActor.java | 29 + .../kontor/media/data/MediaActorFile.java | 34 + .../media/data/MediaActorFileRepository.java | 6 + .../media/data/MediaActorRepository.java | 17 + .../kontor/media/data/MediaArticle.java | 29 + .../media/data/MediaArticleRepository.java | 13 + .../thpeetz/kontor/media/data/MediaFile.java | 44 + .../media/data/MediaFileRepository.java | 14 + .../thpeetz/kontor/media/data/MediaVideo.java | 40 + .../media/data/MediaVideoRepository.java | 15 + .../media/services/MediaArticleService.java | 42 + .../media/services/MediaFileService.java | 90 ++ .../media/services/MediaVideoService.java | 39 + .../media/views/MediaActorFileForm.java | 106 ++ .../media/views/MediaActorFileView.java | 114 ++ .../kontor/media/views/MediaActorForm.java | 117 ++ .../kontor/media/views/MediaActorView.java | 123 ++ .../kontor/media/views/MediaArticleForm.java | 108 ++ .../kontor/media/views/MediaArticleView.java | 144 ++ .../kontor/media/views/MediaFileForm.java | 128 ++ .../kontor/media/views/MediaFileView.java | 167 +++ .../kontor/media/views/MediaVideoForm.java | 113 ++ .../kontor/media/views/MediaVideoView.java | 151 +++ .../kontor/security/SecurityConfig.java | 59 + .../kontor/security/SecurityService.java | 105 ++ .../thpeetz/kontor/tysc/SetupModuleTysc.java | 493 +++++++ .../de/thpeetz/kontor/tysc/TyscConstants.java | 88 ++ .../de/thpeetz/kontor/tysc/data/Card.java | 62 + .../de/thpeetz/kontor/tysc/data/CardSet.java | 41 + .../kontor/tysc/data/FieldPosition.java | 52 + .../de/thpeetz/kontor/tysc/data/Player.java | 49 + .../de/thpeetz/kontor/tysc/data/Rooster.java | 51 + .../de/thpeetz/kontor/tysc/data/Sport.java | 53 + .../de/thpeetz/kontor/tysc/data/Team.java | 49 + .../de/thpeetz/kontor/tysc/data/Vendor.java | 38 + .../tysc/repository/CardRepository.java | 21 + .../tysc/repository/CardSetRepository.java | 20 + .../repository/FieldPositionRepository.java | 24 + .../tysc/repository/PlayerRepository.java | 18 + .../tysc/repository/RoosterRepository.java | 15 + .../tysc/repository/SportRepository.java | 18 + .../tysc/repository/TeamRepository.java | 28 + .../tysc/repository/VendorRepository.java | 18 + .../kontor/tysc/services/CardService.java | 93 ++ .../kontor/tysc/services/SportService.java | 195 +++ .../thpeetz/kontor/tysc/views/CardForm.java | 116 ++ .../kontor/tysc/views/CardSetForm.java | 103 ++ .../kontor/tysc/views/CardSetView.java | 129 ++ .../thpeetz/kontor/tysc/views/CardView.java | 162 +++ .../thpeetz/kontor/tysc/views/PlayerForm.java | 116 ++ .../thpeetz/kontor/tysc/views/PlayerView.java | 130 ++ .../kontor/tysc/views/PositionForm.java | 115 ++ .../kontor/tysc/views/PositionView.java | 130 ++ .../kontor/tysc/views/RoosterForm.java | 117 ++ .../kontor/tysc/views/RoosterView.java | 121 ++ .../thpeetz/kontor/tysc/views/SportForm.java | 103 ++ .../thpeetz/kontor/tysc/views/SportView.java | 129 ++ .../thpeetz/kontor/tysc/views/TeamForm.java | 115 ++ .../thpeetz/kontor/tysc/views/TeamView.java | 130 ++ .../thpeetz/kontor/tysc/views/TyscLayout.java | 41 + .../thpeetz/kontor/tysc/views/VendorForm.java | 115 ++ .../thpeetz/kontor/tysc/views/VendorView.java | 130 ++ .../META-INF/resources/images/offline.png | Bin 0 -> 9507 bytes .../resources/META-INF/resources/offline.html | 38 + .../src/main/resources/application.yml | 87 ++ kontor-spring/src/main/resources/banner.txt | 8 + .../src/main/resources/logback-spring.xml | 42 + .../de/thpeetz/kontor/ApplicationTests.java | 13 + .../kontor/bookshelf/TestConstants.java | 19 + .../data/ArticleAuthorRepositoryTest.java | 74 + .../bookshelf/data/ArticleAuthorTest.java | 77 ++ .../bookshelf/data/ArticleRepositoryTest.java | 61 + .../kontor/bookshelf/data/ArticleTest.java | 45 + .../bookshelf/data/AuthorRepositoryTest.java | 69 + .../kontor/bookshelf/data/AuthorTest.java | 44 + .../data/BookAuthorRepositoryTest.java | 94 ++ .../kontor/bookshelf/data/BookAuthorTest.java | 82 ++ .../bookshelf/data/BookRepositoryTest.java | 87 ++ .../kontor/bookshelf/data/BookTest.java | 56 + .../BookshelfPublisherRepositoryTest.java | 72 + .../data/BookshelfPublisherTest.java | 45 + .../services/BookshelfServiceTest.java | 102 ++ .../kontor/comics/ComicConstantsTest.java | 14 + .../thpeetz/kontor/comics/TestConstants.java | 67 + .../comics/data/ArtistRepositoryTest.java | 73 + .../kontor/comics/data/ArtistTest.java | 48 + .../comics/data/ComicRepositoryTest.java | 53 + .../thpeetz/kontor/comics/data/ComicTest.java | 77 ++ .../comics/data/ComicWorkRepositoryTest.java | 83 ++ .../kontor/comics/data/ComicWorkTest.java | 24 + .../comics/data/IssueRepositoryTest.java | 45 + .../thpeetz/kontor/comics/data/IssueTest.java | 62 + .../comics/data/PublisherRepositoryTest.java | 68 + .../kontor/comics/data/PublisherTest.java | 45 + .../comics/data/StoryArcRepositoryTest.java | 45 + .../kontor/comics/data/StoryArcTest.java | 61 + .../data/TradePaperbackRepositoryTest.java | 59 + .../comics/data/TradePaperbackTest.java | 59 + .../comics/data/VolumeRepositoryTest.java | 61 + .../kontor/comics/data/VolumeTest.java | 64 + .../comics/data/WorktypeRepositoryTest.java | 46 + .../kontor/comics/data/WorktypeTest.java | 83 ++ .../comics/services/ComicServiceTest.java | 350 +++++ .../thpeetz/kontor/media/TestConstants.java | 5 + .../kontor/media/data/MediaArticleTest.java | 32 + .../kontor/media/data/MediaFileTest.java | 37 + .../kontor/media/data/MediaVideoTest.java | 38 + .../services/MediaArticleServiceTest.java | 50 + .../media/services/MediaFileServiceTest.java | 48 + .../media/services/MediaVideoServiceTest.java | 48 + .../de/thpeetz/kontor/tysc/TestConstants.java | 39 + .../thpeetz/kontor/tysc/data/CardSetTest.java | 22 + .../de/thpeetz/kontor/tysc/data/CardTest.java | 22 + .../kontor/tysc/data/FieldPositionTest.java | 45 + .../thpeetz/kontor/tysc/data/PlayerTest.java | 54 + .../thpeetz/kontor/tysc/data/RoosterTest.java | 48 + .../thpeetz/kontor/tysc/data/SportTest.java | 45 + .../de/thpeetz/kontor/tysc/data/TeamTest.java | 54 + .../thpeetz/kontor/tysc/data/VendorTest.java | 45 + .../tysc/repository/CardRepositoryTest.java | 39 + .../repository/CardSetRepositoryTest.java | 44 + .../FieldPositionRepositoryTest.java | 61 + .../tysc/repository/PlayerRepositoryTest.java | 32 + .../repository/RoosterRepositoryTest.java | 33 + .../tysc/repository/SportRepositoryTest.java | 41 + .../tysc/repository/TeamRepositoryTest.java | 38 + .../tysc/repository/VendorRepositoryTest.java | 42 + .../kontor/tysc/services/CardServiceTest.java | 126 ++ .../tysc/services/SportServiceTest.java | 200 +++ .../src/test/resources/application.properties | 30 + kontor.tjp | 30 + kotlin-quarkus/.dockerignore | 5 + kotlin-quarkus/.gitignore | 36 + kotlin-quarkus/README.md | 78 ++ kotlin-quarkus/build.gradle.kts | 45 + kotlin-quarkus/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 58910 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + kotlin-quarkus/gradlew | 185 +++ kotlin-quarkus/gradlew.bat | 104 ++ kotlin-quarkus/settings.gradle.kts | 13 + kotlin-quarkus/src/main/docker/Dockerfile.jvm | 94 ++ .../src/main/docker/Dockerfile.legacy-jar | 90 ++ .../src/main/docker/Dockerfile.native | 27 + .../src/main/docker/Dockerfile.native-micro | 30 + .../resources/META-INF/resources/index.html | 289 ++++ .../src/main/resources/application.yml | 2 + .../de/thpeetz/kontor/GreetingResourceIT.java | 8 + kotlin-spring/.gitignore | 37 + kotlin-spring/README.md | 92 ++ kotlin-spring/build.gradle | 58 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + kotlin-spring/gradlew | 240 ++++ kotlin-spring/gradlew.bat | 91 ++ kotlin-spring/settings.gradle | 1 + .../de/thpeetz/kontor/KontorApplication.kt | 16 + .../de/thpeetz/kontor/KontorConfiguration.kt | 79 ++ .../de/thpeetz/kontor/KontorController.kt | 24 + .../de/thpeetz/kontor/KontorProperties.kt | 10 + .../kotlin/de/thpeetz/kontor/Navigation.kt | 24 + .../de/thpeetz/kontor/RequestLoggingFilter.kt | 25 + .../kontor/comics/ComicsApiController.kt | 25 + .../thpeetz/kontor/comics/ComicsController.kt | 29 + .../de/thpeetz/kontor/comics/Entities.kt | 36 + .../de/thpeetz/kontor/comics/Repositories.kt | 22 + .../src/main/resources/application.properties | 6 + .../main/resources/templates/comics.mustache | 23 + .../main/resources/templates/footer.mustache | 22 + .../main/resources/templates/header.mustache | 15 + .../main/resources/templates/kontor.mustache | 18 + .../main/resources/templates/menu.mustache | 23 + .../test/resources/junit-platform.properties | 1 + wicket/.github/workflows/gradle.yml | 26 + wicket/.gitignore | 7 + wicket/.gitlab-ci.yml | 36 + wicket/README.md | 4 + wicket/build.gradle | 61 + wicket/gradle.properties | 3 + wicket/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + wicket/gradlew | 240 ++++ wicket/gradlew.bat | 91 ++ wicket/settings.gradle | 1 + wicket/src/docs/asciidoc/kontor-wicket.adoc | 179 +++ .../main/java/de/thpeetz/kontor/HomePage.java | 18 + .../de/thpeetz/kontor/KontorApplication.java | 41 + .../resources/de/thpeetz/kontor/HomePage.html | 62 + wicket/src/main/webapp/WEB-INF/web.xml | 32 + wicket/src/main/webapp/logo.png | Bin 0 -> 12244 bytes wicket/src/main/webapp/style.css | 68 + .../test/java/de/thpeetz/kontor/Start.java | 106 ++ .../java/de/thpeetz/kontor/TestHomePage.java | 29 + .../src/test/resources/jetty/jetty-http.xml | 38 + .../src/test/resources/jetty/jetty-https.xml | 45 + wicket/src/test/resources/jetty/jetty-ssl.xml | 57 + wicket/src/test/resources/jetty/jetty.xml | 23 + wicket/src/test/resources/keystore | Bin 0 -> 3954 bytes 1043 files changed, 61259 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 docker-compose.yml create mode 100644 go/.env create mode 100644 go/.gitignore create mode 100644 go/.gitlab-ci.yml create mode 100644 go/Makefile create mode 100644 go/README.md create mode 100644 go/cmd/kontor/main.go create mode 100644 go/cmd/kontor/model.go create mode 100644 go/cmd/root.go create mode 100644 go/cmd/server.go create mode 100644 go/comics.xml create mode 100644 go/docs/build.gradle create mode 100644 go/docs/gradle.properties create mode 100644 go/docs/gradle/wrapper/gradle-wrapper.jar create mode 100644 go/docs/gradle/wrapper/gradle-wrapper.properties create mode 100755 go/docs/gradlew create mode 100644 go/docs/gradlew.bat create mode 100644 go/docs/settings.gradle create mode 100644 go/docs/src/docs/asciidoc/kontor-go.adoc create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/kontor-ansible.yml create mode 100644 go/pkg/admin/routes.go create mode 100644 go/pkg/admin/user.go create mode 100644 go/pkg/admin/user_dao.go create mode 100644 go/pkg/admin/user_test.go create mode 100644 go/pkg/admin/views.go create mode 100644 go/pkg/application/registry/comic/registry.go create mode 100644 go/pkg/auth/middleware.go create mode 100644 go/pkg/auth/session.go create mode 100644 go/pkg/auth/session_dao.go create mode 100644 go/pkg/auth/session_test.go create mode 100644 go/pkg/comics/artist.go create mode 100644 go/pkg/comics/artist_dao.go create mode 100644 go/pkg/comics/artist_test.go create mode 100644 go/pkg/comics/artist_views.go create mode 100644 go/pkg/comics/comic.go create mode 100644 go/pkg/comics/comic_dao.go create mode 100644 go/pkg/comics/comic_test.go create mode 100644 go/pkg/comics/publisher.go create mode 100644 go/pkg/comics/publisher_dao.go create mode 100644 go/pkg/comics/publisher_test.go create mode 100644 go/pkg/comics/publisher_views.go create mode 100644 go/pkg/comics/routes.go create mode 100644 go/pkg/comics/views.go create mode 100644 go/pkg/context/comic/responses/comic.go create mode 100644 go/pkg/context/comic/routing/endpoints.go create mode 100644 go/pkg/dao/database.go create mode 100644 go/pkg/dao/database_test.go create mode 100644 go/pkg/infrastructure/app/factory.go create mode 100644 go/pkg/infrastructure/config/factory.go create mode 100644 go/pkg/infrastructure/kernel/app.go create mode 100644 go/pkg/infrastructure/response/response.go create mode 100644 go/pkg/library/author.go create mode 100644 go/pkg/library/author_dao.go create mode 100644 go/pkg/library/author_test.go create mode 100644 go/pkg/library/book.go create mode 100644 go/pkg/library/book_dao.go create mode 100644 go/pkg/library/book_test.go create mode 100644 go/pkg/library/publisher.go create mode 100644 go/pkg/library/publisher_dao.go create mode 100644 go/pkg/library/publisher_test.go create mode 100644 go/pkg/library/publisher_views.go create mode 100644 go/pkg/library/routes.go create mode 100644 go/pkg/library/views.go create mode 100644 go/pkg/office/routes.go create mode 100644 go/pkg/office/views.go create mode 100644 go/pkg/properties/root.go create mode 100644 go/pkg/setup/data.go create mode 100644 go/pkg/setup/routes.go create mode 100644 go/pkg/setup/session.go create mode 100644 go/pkg/setup/user.go create mode 100644 go/pkg/tradingcards/routes.go create mode 100644 go/pkg/tradingcards/views.go create mode 100644 go/pkg/tysc/card.go create mode 100644 go/pkg/tysc/card_dao.go create mode 100644 go/pkg/tysc/card_test.go create mode 100644 go/pkg/tysc/cardset.go create mode 100644 go/pkg/tysc/cardset_dao.go create mode 100644 go/pkg/tysc/cardset_test.go create mode 100644 go/pkg/tysc/insertset.go create mode 100644 go/pkg/tysc/insertset_dao.go create mode 100644 go/pkg/tysc/insertset_test.go create mode 100644 go/pkg/tysc/manufacturer.go create mode 100644 go/pkg/tysc/manufacturer_dao.go create mode 100644 go/pkg/tysc/manufacturer_test.go create mode 100644 go/pkg/tysc/parallelset.go create mode 100644 go/pkg/tysc/parallelset_dao.go create mode 100644 go/pkg/tysc/parallelset_test.go create mode 100644 go/pkg/tysc/player.go create mode 100644 go/pkg/tysc/player_dao.go create mode 100644 go/pkg/tysc/player_test.go create mode 100644 go/pkg/tysc/position.go create mode 100644 go/pkg/tysc/position_dao.go create mode 100644 go/pkg/tysc/position_test.go create mode 100644 go/pkg/tysc/routes.go create mode 100644 go/pkg/tysc/sport.go create mode 100644 go/pkg/tysc/sport_dao.go create mode 100644 go/pkg/tysc/sport_test.go create mode 100644 go/pkg/tysc/team.go create mode 100644 go/pkg/tysc/team_dao.go create mode 100644 go/pkg/tysc/team_test.go create mode 100644 go/pkg/tysc/views.go create mode 100644 go/pkg/util/render.go create mode 100644 go/sonar-project.properties create mode 100644 go/templates/comics/artists.html create mode 100644 go/templates/comics/comic.html create mode 100644 go/templates/comics/comics.html create mode 100644 go/templates/comics/menu.html create mode 100644 go/templates/comics/publisher.html create mode 100644 go/templates/comics/publishers.html create mode 100644 go/templates/kontor/admin-menu.html create mode 100644 go/templates/kontor/admin.html create mode 100644 go/templates/kontor/create-user.html create mode 100644 go/templates/kontor/data-upload.html create mode 100644 go/templates/kontor/footer.html create mode 100644 go/templates/kontor/header.html create mode 100644 go/templates/kontor/index.html create mode 100644 go/templates/kontor/login-successful.html create mode 100644 go/templates/kontor/login.html create mode 100644 go/templates/kontor/menu.html create mode 100644 go/templates/kontor/user-detail.html create mode 100644 go/templates/kontor/users.html create mode 100644 go/templates/library/authors.html create mode 100644 go/templates/library/books.html create mode 100644 go/templates/library/menu.html create mode 100644 go/templates/library/publisher.html create mode 100644 go/templates/library/publishers.html create mode 100644 go/templates/office/index.html create mode 100644 go/templates/office/menu.html create mode 100644 go/templates/tradingcards/index.html create mode 100644 go/templates/tradingcards/menu.html create mode 100644 go/templates/tysc/index.html create mode 100644 go/templates/tysc/menu.html create mode 100644 go/templates/tysc/sports.html create mode 100644 go/tysc-20041010-1819.sql create mode 100644 java-ee/ComicsImpl/build.gradle create mode 100644 java-ee/ComicsImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/ComicsImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java create mode 100644 java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java create mode 100644 java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java create mode 100644 java-ee/ComicsWeb/build.gradle create mode 100644 java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/artistList.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comicList.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/comics.xhtml create mode 100644 java-ee/ComicsWeb/src/main/webapp/index.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp create mode 100644 java-ee/ComicsWeb/src/main/webapp/publisherList.jsp create mode 100644 java-ee/DVDs.csv create mode 100644 java-ee/Jenkinsfile create mode 100644 java-ee/KontorApp/build.gradle create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java create mode 100644 java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java create mode 100644 java-ee/KontorApp/src/main/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorApp/src/main/resources/logback.xml create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java create mode 100644 java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java create mode 100644 java-ee/KontorApp/src/test/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorApp/src/test/resources/logback.xml create mode 100755 java-ee/KontorEJB/build.gradle create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java create mode 100755 java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java create mode 100644 java-ee/KontorImpl/build.gradle create mode 100644 java-ee/KontorImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/KontorImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/KontorImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java create mode 100644 java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java create mode 100755 java-ee/KontorWeb/build.gradle create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportComics.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportLibrary.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportMedien.java create mode 100644 java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java create mode 100755 java-ee/KontorWeb/src/main/resources/META-INF/persistence.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml create mode 100644 java-ee/KontorWeb/src/main/webapp/comics.xhtml create mode 100755 java-ee/KontorWeb/src/main/webapp/css/store.css create mode 100755 java-ee/KontorWeb/src/main/webapp/index.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/library.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/medien.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css create mode 100644 java-ee/KontorWeb/src/main/webapp/resources/css/default.css create mode 100644 java-ee/KontorWeb/src/main/webapp/seite.html create mode 100644 java-ee/KontorWeb/src/main/webapp/sport.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml create mode 100644 java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml create mode 100644 java-ee/LibraryImpl/build.gradle create mode 100644 java-ee/LibraryImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/LibraryImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java create mode 100644 java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java create mode 100644 java-ee/LibraryWeb/build.gradle create mode 100644 java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java create mode 100644 java-ee/LibraryWeb/src/main/webapp/index.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp create mode 100644 java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp create mode 100644 java-ee/MedienImpl/build.gradle create mode 100644 java-ee/MedienImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/MedienImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/MedienImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java create mode 100644 java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java create mode 100644 java-ee/MedienWeb/build.gradle create mode 100644 java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java create mode 100644 java-ee/MedienWeb/src/main/webapp/index.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp create mode 100644 java-ee/MedienWeb/src/main/webapp/jsp/index.jsp create mode 100644 java-ee/README.md create mode 100644 java-ee/TradingCardsImpl/build.gradle create mode 100644 java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml create mode 100644 java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/TradingCardsImpl/config/findbugs/findbugs.xml create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java create mode 100644 java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java create mode 100644 java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java-ee/TradingCardsWeb/build.gradle create mode 100644 java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java create mode 100644 java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java create mode 100644 java-ee/TradingCardsWeb/src/main/webapp/index.jsp create mode 100644 java-ee/build.gradle create mode 100644 java-ee/comics.xml create mode 100644 java-ee/config/checkstyle/checkstyle.xml create mode 100644 java-ee/config/checkstyle/checkstyle.xsl create mode 100644 java-ee/config/findbugs/findbugs.xml create mode 100644 java-ee/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-ee/gradle/wrapper/gradle-wrapper.properties create mode 100755 java-ee/gradlew create mode 100755 java-ee/gradlew.bat create mode 100755 java-ee/settings.gradle create mode 100644 java-quarkus/.gitignore create mode 100644 java-quarkus/.gitlab-ci.yml create mode 100644 java-quarkus/README.md create mode 100644 java-quarkus/build.gradle create mode 100644 java-quarkus/config/checkstyle/checkstyle.xml create mode 100644 java-quarkus/gradle.properties create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.jar create mode 100644 java-quarkus/gradle/wrapper/gradle-wrapper.properties create mode 100644 java-quarkus/gradlew create mode 100644 java-quarkus/gradlew.bat create mode 100644 java-quarkus/settings.gradle create mode 100644 java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc create mode 100644 java-quarkus/src/main/docker/Dockerfile.jvm create mode 100644 java-quarkus/src/main/docker/Dockerfile.legacy-jar create mode 100644 java-quarkus/src/main/docker/Dockerfile.native create mode 100644 java-quarkus/src/main/docker/Dockerfile.native-micro create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java create mode 100644 java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java create mode 100644 java-quarkus/src/main/resources/META-INF/resources/index.html create mode 100644 java-quarkus/src/main/resources/application.properties create mode 100644 java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java create mode 100644 java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java create mode 100644 java/README.md create mode 100644 java/build.gradle create mode 100644 java/comics.xml create mode 100644 java/config/checkstyle/checkstyle.xml create mode 100644 java/config/checkstyle/checkstyle.xsl create mode 100644 java/config/findbugs/findbugs.xml create mode 100644 java/gradle.properties create mode 100644 java/gradle/wrapper/gradle-wrapper.jar create mode 100644 java/gradle/wrapper/gradle-wrapper.properties create mode 100755 java/gradlew create mode 100644 java/gradlew.bat create mode 100644 java/settings.gradle create mode 100644 java/src/main/java/com/ibtp/kontor/Database.java create mode 100644 java/src/main/java/com/ibtp/kontor/DumpComics.java create mode 100644 java/src/main/java/com/ibtp/kontor/KontorApp.java create mode 100644 java/src/main/java/com/ibtp/kontor/KontorGUI.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/Database.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java create mode 100644 java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java create mode 100644 java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java create mode 100644 java/src/main/resources/META-INF/persistence.xml create mode 100644 java/src/main/resources/logback.xml create mode 100644 java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java create mode 100644 java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java create mode 100644 java/src/test/resources/META-INF/persistence.xml create mode 100644 java/src/test/resources/logback.xml create mode 100644 java/tysc-20041010-1819.sql create mode 100644 kontor-api/.coverage create mode 100644 kontor-api/.gitignore create mode 100644 kontor-api/.python-version create mode 100644 kontor-api/Dockerfile create mode 100644 kontor-api/Makefile create mode 100644 kontor-api/README.md create mode 100644 kontor-api/pyproject.toml create mode 100644 kontor-api/src/__init__.py create mode 100644 kontor-api/src/apis/__init__.py create mode 100644 kontor-api/src/apis/base.py create mode 100644 kontor-api/src/apis/utils.py create mode 100644 kontor-api/src/apis/version1/__init__.py create mode 100644 kontor-api/src/apis/version1/comic.py create mode 100644 kontor-api/src/apis/version1/media.py create mode 100644 kontor-api/src/apis/version1/tysc.py create mode 100644 kontor-api/src/core/__init__.py create mode 100644 kontor-api/src/core/config.py create mode 100644 kontor-api/src/db/__init__.py create mode 100644 kontor-api/src/db/models/__init__.py create mode 100644 kontor-api/src/db/models/admin.py create mode 100644 kontor-api/src/db/models/base.py create mode 100644 kontor-api/src/db/models/bookshelf.py create mode 100644 kontor-api/src/db/models/comic.py create mode 100644 kontor-api/src/db/models/database.py create mode 100644 kontor-api/src/db/models/media.py create mode 100644 kontor-api/src/db/models/metadata.py create mode 100644 kontor-api/src/db/models/tysc.py create mode 100644 kontor-api/src/db/session.py create mode 100644 kontor-api/src/main.py create mode 100644 kontor-api/src/schema/__init__.py create mode 100644 kontor-api/src/schema/comics/__init__.py create mode 100644 kontor-api/src/schema/comics/artist.py create mode 100644 kontor-api/src/schema/comics/comic.py create mode 100644 kontor-api/src/schema/media/__init__.py create mode 100644 kontor-api/src/schema/media/file.py create mode 100644 kontor-api/src/schema/tysc/__init__.py create mode 100644 kontor-api/src/schema/tysc/sport.py create mode 100644 kontor-api/src/static/images/cross.png create mode 100644 kontor-api/src/static/images/logo.png create mode 100644 kontor-api/src/static/images/tick.png create mode 100644 kontor-api/src/static/js/autocomplete.js create mode 100644 kontor-api/src/templates/comic/artist_detail.html create mode 100644 kontor-api/src/templates/comic/artists.html create mode 100644 kontor-api/src/templates/comic/comic_detail.html create mode 100644 kontor-api/src/templates/comic/comics.html create mode 100644 kontor-api/src/templates/comic/publisher_detail.html create mode 100644 kontor-api/src/templates/comic/publishers.html create mode 100644 kontor-api/src/templates/components/actor_cards.html create mode 100644 kontor-api/src/templates/components/alerts.html create mode 100644 kontor-api/src/templates/components/artist_cards.html create mode 100644 kontor-api/src/templates/components/check.html create mode 100644 kontor-api/src/templates/components/comic_cards.html create mode 100644 kontor-api/src/templates/components/navbar.html create mode 100644 kontor-api/src/templates/components/publisher_cards.html create mode 100644 kontor-api/src/templates/index.html create mode 100644 kontor-api/src/templates/media/actor_detail.html create mode 100644 kontor-api/src/templates/media/actors.html create mode 100644 kontor-api/src/templates/media/file_detail.html create mode 100644 kontor-api/src/templates/media/files.html create mode 100644 kontor-api/src/templates/shared/base.html create mode 100644 kontor-api/src/webapps/__init__.py create mode 100644 kontor-api/src/webapps/base.py create mode 100644 kontor-api/src/webapps/comic/__init__.py create mode 100644 kontor-api/src/webapps/comic/route_comics.py create mode 100644 kontor-api/src/webapps/media/route_media.py create mode 100644 kontor-api/tests/__init__.py create mode 100644 kontor-api/tests/test_main.py create mode 100644 kontor-api/uv.lock create mode 100644 kontor-gui/.gitignore create mode 100644 kontor-gui/Makefile create mode 100644 kontor-gui/README.md create mode 100644 kontor-gui/gui/__init__.py create mode 100644 kontor-gui/gui/comic_window.py create mode 100644 kontor-gui/gui/data_view.py create mode 100644 kontor-gui/gui/data_view_model.py create mode 100644 kontor-gui/gui/dialogs.py create mode 100644 kontor-gui/gui/main_window.py create mode 100644 kontor-gui/gui/media_window.py create mode 100644 kontor-gui/gui/meta_data_window.py create mode 100644 kontor-gui/gui/model_config.py create mode 100644 kontor-gui/gui/progress.py create mode 100644 kontor-gui/gui/table_details.py create mode 100644 kontor-gui/gui/table_model.py create mode 100644 kontor-gui/gui/worker.py create mode 100644 kontor-gui/main.py create mode 100644 kontor-gui/pyproject.toml create mode 100644 kontor-gui/pysidedeploy.spec create mode 100644 kontor-gui/pyvenv.cfg create mode 100644 kontor-gui/requirements.txt create mode 100644 kontor-gui/res/application-export.png create mode 100644 kontor-gui/res/application-import.png create mode 100644 kontor-gui/res/arrow-circle-double.png create mode 100644 kontor-gui/res/cross.png create mode 100644 kontor-gui/res/tick.png create mode 100644 kontor-gui/src/kontor_gui/__init__.py create mode 100644 kontor-gui/tests/__init__.py create mode 100644 kontor-schema/.gitignore create mode 100644 kontor-schema/.python-version create mode 100644 kontor-schema/README.md create mode 100644 kontor-schema/pyproject.toml create mode 100644 kontor-schema/src/kontor_schema/__init__.py create mode 100644 kontor-schema/src/kontor_schema/admin.py create mode 100644 kontor-schema/src/kontor_schema/base.py create mode 100644 kontor-schema/src/kontor_schema/bookshelf.py create mode 100644 kontor-schema/src/kontor_schema/comic.py create mode 100644 kontor-schema/src/kontor_schema/database.py create mode 100644 kontor-schema/src/kontor_schema/media.py create mode 100644 kontor-schema/src/kontor_schema/metadata.py create mode 100644 kontor-schema/src/kontor_schema/tysc.py create mode 100644 kontor-schema/uv.lock create mode 100644 kontor-scripts/.python-version create mode 100644 kontor-scripts/Makefile create mode 100644 kontor-scripts/README.md create mode 100644 kontor-scripts/check_kontor.py create mode 100644 kontor-scripts/config.py create mode 100644 kontor-scripts/copy_to_mariadb.py create mode 100644 kontor-scripts/copy_to_sqlite.py create mode 100644 kontor-scripts/db_structure.py create mode 100644 kontor-scripts/download.py create mode 100644 kontor-scripts/export.py create mode 100644 kontor-scripts/import.py create mode 100644 kontor-scripts/json_to_mariadb.py create mode 100644 kontor-scripts/pyproject.toml create mode 100644 kontor-scripts/read_list.py create mode 100644 kontor-scripts/requirements.txt create mode 100644 kontor-scripts/schema/__init__.py create mode 100644 kontor-scripts/schema/admin.py create mode 100644 kontor-scripts/schema/base.py create mode 100644 kontor-scripts/schema/bookshelf.py create mode 100644 kontor-scripts/schema/comic.py create mode 100644 kontor-scripts/schema/database.py create mode 100644 kontor-scripts/schema/media.py create mode 100644 kontor-scripts/schema/metadata.py create mode 100644 kontor-scripts/schema/tysc.py create mode 100644 kontor-scripts/update_title.py create mode 100644 kontor-scripts/uv.lock create mode 100644 kontor-spring/.factorypath create mode 100644 kontor-spring/.gitattributes create mode 100644 kontor-spring/.gitignore create mode 100644 kontor-spring/Dockerfile create mode 100644 kontor-spring/Makefile create mode 100644 kontor-spring/README.md create mode 100644 kontor-spring/build.gradle create mode 100644 kontor-spring/frontend/themes/kontor/styles.css create mode 100644 kontor-spring/frontend/themes/kontor/theme.json create mode 100644 kontor-spring/gradle.properties create mode 100644 kontor-spring/gradle/libs.versions.toml create mode 100644 kontor-spring/gradle/wrapper/gradle-wrapper.jar create mode 100644 kontor-spring/gradle/wrapper/gradle-wrapper.properties create mode 100755 kontor-spring/gradlew create mode 100644 kontor-spring/gradlew.bat create mode 100644 kontor-spring/settings.gradle create mode 100644 kontor-spring/src/docs/asciidoc/kontor-spring.adoc create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java create mode 100644 kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java create mode 100644 kontor-spring/src/integrationTest/resources/application.properties create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/Application.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/MailProperties.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Assignment.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Permission.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Profile.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Token.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/AssignmentRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MailAccountRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataColumnRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataTableRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ModuleDataRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/PermissionRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ProfileRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MailService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Artist.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Comic.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Issue.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Volume.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/ColumnToggleContextMenu.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/common/views/StatusIcon.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/data/services/DataManagementService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/data/views/DataManagementView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/data/views/ImportArea.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/data/views/UploadArea.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/MediaConstants.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Card.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Player.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Team.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardSetRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/PlayerRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/RoosterRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/SportRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/TeamRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/VendorRepository.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java create mode 100644 kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java create mode 100644 kontor-spring/src/main/resources/META-INF/resources/images/offline.png create mode 100644 kontor-spring/src/main/resources/META-INF/resources/offline.html create mode 100644 kontor-spring/src/main/resources/application.yml create mode 100644 kontor-spring/src/main/resources/banner.txt create mode 100644 kontor-spring/src/main/resources/logback-spring.xml create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/ApplicationTests.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/TestConstants.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/TestConstants.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardSetRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/PlayerRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/RoosterRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/SportRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/TeamRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/VendorRepositoryTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java create mode 100644 kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java create mode 100644 kontor-spring/src/test/resources/application.properties create mode 100644 kontor.tjp create mode 100644 kotlin-quarkus/.dockerignore create mode 100644 kotlin-quarkus/.gitignore create mode 100644 kotlin-quarkus/README.md create mode 100644 kotlin-quarkus/build.gradle.kts create mode 100644 kotlin-quarkus/gradle.properties create mode 100644 kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar create mode 100644 kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties create mode 100755 kotlin-quarkus/gradlew create mode 100644 kotlin-quarkus/gradlew.bat create mode 100644 kotlin-quarkus/settings.gradle.kts create mode 100644 kotlin-quarkus/src/main/docker/Dockerfile.jvm create mode 100644 kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar create mode 100644 kotlin-quarkus/src/main/docker/Dockerfile.native create mode 100644 kotlin-quarkus/src/main/docker/Dockerfile.native-micro create mode 100644 kotlin-quarkus/src/main/resources/META-INF/resources/index.html create mode 100644 kotlin-quarkus/src/main/resources/application.yml create mode 100644 kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java create mode 100644 kotlin-spring/.gitignore create mode 100644 kotlin-spring/README.md create mode 100644 kotlin-spring/build.gradle create mode 100644 kotlin-spring/gradle/wrapper/gradle-wrapper.jar create mode 100644 kotlin-spring/gradle/wrapper/gradle-wrapper.properties create mode 100644 kotlin-spring/gradlew create mode 100644 kotlin-spring/gradlew.bat create mode 100644 kotlin-spring/settings.gradle create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt create mode 100644 kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt create mode 100644 kotlin-spring/src/main/resources/application.properties create mode 100644 kotlin-spring/src/main/resources/templates/comics.mustache create mode 100644 kotlin-spring/src/main/resources/templates/footer.mustache create mode 100644 kotlin-spring/src/main/resources/templates/header.mustache create mode 100644 kotlin-spring/src/main/resources/templates/kontor.mustache create mode 100644 kotlin-spring/src/main/resources/templates/menu.mustache create mode 100644 kotlin-spring/src/test/resources/junit-platform.properties create mode 100644 wicket/.github/workflows/gradle.yml create mode 100644 wicket/.gitignore create mode 100644 wicket/.gitlab-ci.yml create mode 100644 wicket/README.md create mode 100644 wicket/build.gradle create mode 100644 wicket/gradle.properties create mode 100644 wicket/gradle/wrapper/gradle-wrapper.jar create mode 100644 wicket/gradle/wrapper/gradle-wrapper.properties create mode 100755 wicket/gradlew create mode 100644 wicket/gradlew.bat create mode 100644 wicket/settings.gradle create mode 100644 wicket/src/docs/asciidoc/kontor-wicket.adoc create mode 100644 wicket/src/main/java/de/thpeetz/kontor/HomePage.java create mode 100644 wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java create mode 100644 wicket/src/main/resources/de/thpeetz/kontor/HomePage.html create mode 100644 wicket/src/main/webapp/WEB-INF/web.xml create mode 100644 wicket/src/main/webapp/logo.png create mode 100644 wicket/src/main/webapp/style.css create mode 100644 wicket/src/test/java/de/thpeetz/kontor/Start.java create mode 100644 wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java create mode 100644 wicket/src/test/resources/jetty/jetty-http.xml create mode 100644 wicket/src/test/resources/jetty/jetty-https.xml create mode 100644 wicket/src/test/resources/jetty/jetty-ssl.xml create mode 100644 wicket/src/test/resources/jetty/jetty.xml create mode 100644 wicket/src/test/resources/keystore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57bd15d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +.idea/ +.theia/ +.vscode/ +__pycache__/ +bonus/ +icons/ +icons-shadowless/ +springboot/.factorypath +java-ee/.settings +java-ee/.project +kontor-schema/env +kontor-schema/kontor_schema.egg-info +kontor-gui/.pdm-python +kontor-gui/dist +fastapi/.coverage +db-password.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cfa22c --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2024 Kontor + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc51f4a --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +kontor_api := kontor-api +kontor_spring := kontor-spring + +.PHONY: all $(kontor_spring) $(kontor_api) +all: $(kontor_spring) $(kontor_api) + +$(kontor_spring) $(kontor_api): + $(MAKE) --directory=$@ $(TARGET) + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..55e906d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ + +services: + mariadb: + image: mariadb + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: kontor + MYSQL_USER: kontor + MYSQL_PASSWORD: kontor + MYSQL_DATABASE: kontor + ports: + - 3316:3306 + networks: + - database + volumes: + - mariadb-storage:/var/lib/mysql:rw + kontor: + image: kontor + restart: unless-stopped + networks: + - database + - frontend + ports: + - 8000:8000 + depends_on: + - mariadb + kontor-api: + image: kontor-api + restart: unless-stopped + networks: + - database + - frontend + ports: + - 8800:8800 + depends_on: + - mariadb + +networks: + database: + frontend: + +volumes: + mariadb-storage: + diff --git a/go/.env b/go/.env new file mode 100644 index 0000000..33a4c5f --- /dev/null +++ b/go/.env @@ -0,0 +1,10 @@ +APP_NAME="Kontor" +APP_VERSION="v0.1.0" + +# HTTP Response Content-Type Header - Success +HTTP_CONTENT_TYPE="application/vnd.api+json" + +# HTTP Response Content-Type Header - Error +HTTP_PROBLEM="application/problem+json" + +HTTP_PORT=":8086" diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..0303555 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1,9 @@ +.gradle/ +docs/build/ +bin/ +build/ +cover.out +coverage.html +coverage.xml +.settings/ +.project diff --git a/go/.gitlab-ci.yml b/go/.gitlab-ci.yml new file mode 100644 index 0000000..1580961 --- /dev/null +++ b/go/.gitlab-ci.yml @@ -0,0 +1,116 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk update + - sdk d java 11.0.12-open + +stages: +- environment +- prepare +- build +- test +- analysis +- deploy + +environment: + stage: environment + script: + - echo "PATH=$PATH:/usr/local/go/bin:/home/gitlab-runner/go/bin" >> build.env + artifacts: + reports: + dotenv: build.env + +Prepare Go dependencies: + stage: prepare + script: + - go get -u github.com/spf13/cobra + - go get -u github.com/jstemmer/go-junit-report + - go get -u github.com/inconshreveable/mousetrap + - go get -u github.com/mitchellh/go-homedir + - go get github.com/boumenot/gocover-cobertura + - go get -u gotest.tools/gotestsum + - go get -u github.com/cryptix/wav + - go get -u golang.org/x/lint/golint + dependencies: + - environment + +Create Documentation: + stage: build + script: + - chmod +x docs/gradlew + - cd docs; ./gradlew publish + +Compile Go Application: + stage: build + script: make build + +Test Go Application: + stage: test + script: + - gotestsum --junitfile report.xml --format testname -- -coverprofile=coverage.txt -covermode count ./... + - go vet ./... 2> govet-report.out + - gocover-cobertura < coverage.txt > coverage.xml + - golint ./... > golint-report.out + dependencies: + - environment + artifacts: + when: always + paths: + - coverage.xml + - report.xml + reports: + junit: report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml + +Code analysis: + stage: analysis + script: + - go test -coverprofile cover.out ./... -json > report.json + - go vet ./... 2> govet-report.out + - golint ./... > golint-report.out + - echo "sonar.projectKey=kontor_kontor-go_AX-cQT62rXuu6JVRvr-z" >> sonar-project.properties + - echo "sonar.sources=." >> sonar-project.properties + - echo "sonar.exclusions=**/*_test.go" >> sonar-project.properties + - echo "sonar.tests=." >> sonar-project.properties + - echo "sonar.test.inclusions=**/*_test.go" >> sonar-project.properties + - echo "sonar.go.tests.reportPaths=report.json" >> sonar-project.properties + - echo "sonar.go.coverage.reportPaths=cover.out" >> sonar-project.properties + - echo "sonar.go.govet.reportPaths=govet-report.out" >> sonar-project.properties + - echo "sonar.go.golint.reportPaths=golint-report.out" >> sonar-project.properties + - /data/sonar-scanner/bin/sonar-scanner -Dsonar.projectVersion=$(git describe --abbrev=0) -Dsonar.host.url=https://sonar.thpeetz.de -Dsonar.login=319616a2761ac3e96a1c7aacc54976bfff4096a9 + dependencies: + - environment + +Deploy To Staging: + stage: deploy + script: + - make build + - ssh kontor /home/kontor/kontor-test_service stop + - rsync -av templates kontor:/home/kontor/staging + - rsync -av bin/kontor kontor:/home/kontor/staging + - ssh kontor /home/kontor/kontor-test_service start + environment: + name: staging + url: https://kontor-test.thpeetz.de + only: + - main + +Deploy to Production: + stage: deploy + script: + - make build + - sudo service kontor stop + - rsync -av templates kontor:/home/kontor/production + - rsync -av bin/kontor kontor:/home/kontor/production + - sudo service kontor start + environment: + name: production + url: https://kontor.thpeetz.de + + only: + - main + when: manual diff --git a/go/Makefile b/go/Makefile new file mode 100644 index 0000000..7f29013 --- /dev/null +++ b/go/Makefile @@ -0,0 +1,73 @@ +# Go parameters +GOPATH=$(HOME)/go +#GOBIN=$(shell pwd)/bin +GOBIN=/usr/local/go/bin +GOCMD=$(GOBIN)/go +GOGET=$(GOCMD) get +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOCOVER=$(GOCMD) tool cover +COBERTURA=$(HOME)/go/bin/gocover-cobertura +GOLINT=$(GOBIN)/golint +GOVET=$(GOCMD) vet + +# Project parameters +GONAME=kontor +GOFILE=cmd/kontor/main.go + +#.PHONY: all deps clean clean-bin clean-doc + +all: deps build + +deps: + $(GOGET) -u -v github.com/jstemmer/go-junit-report + $(GOGET) -u -v github.com/tebeka/go2xunit + $(GOGET) -u -v github.com/t-yuki/gocover-cobertura + $(GOGET) -u -v github.com/spf13/cobra/cobra + $(GOGET) -u -v github.com/inconshreveable/mousetrap + $(GOGET) -u -v github.com/mitchellh/go-homedir + $(GOGET) -u -v github.com/golang/protobuf/proto + $(GOGET) -u -v github.com/gin-gonic/gin + $(GOGET) -u -v github.com/gin-gonic/gin/binding + $(GOGET) -u -v github.com/gin-gonic/gin/render + $(GOGET) -u -v github.com/gin-contrib/sse + $(GOGET) -u -v github.com/mattn/go-isatty + $(GOGET) -u -v github.com/ugorji/go/codec + $(GOGET) -u -v golang.org/x/crypto/bcrypt + $(GOGET) -u -v golang.org/x/crypto/blowfish + G$(GOGET) -u -v gopkg.in/yaml.v2 + $(GOGET) -u -v gopkg.in/mgo.v2 + $(GOGET) -u -v gopkg.in/mgo.v2/bson + +build: bin/$(GONAME) + +bin/$(GONAME): $(GOFILE) + @echo "Building $(GOFILE) to ./bin" + $(GOBUILD) -v -ldflags="-X main.version=$(shell git describe --always --long --dirty)" -o bin/$(GONAME) $(GOFILE) + +install: + @echo using $(GOPATH) + $(GOCMD) install -v -ldflags="-X main.version=$(shell git describe --always --long --dirty)" ./... + +test: + $(GOTEST) -v ./... + $(GOTEST) -coverprofile=cover.out ./... + $(GOCOVER) -html=cover.out -o coverage.html + $(COBERTURA) < cover.out > coverage.xml + $(GOLINT) ./... + $(GOVET) -v ./... + +doc: + cd docs; ./gradlew build + +clean: clean-doc clean-bin + + +clean-doc: + @echo "Cleaning Gradle build" + cd docs; ./gradlew clean + +clean-bin: + @echo "Cleaning Go build" + rm -rf bin/ diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..6814615 --- /dev/null +++ b/go/README.md @@ -0,0 +1,4 @@ +[![pipeline status](https://gitlab.thpeetz.de/kontor/kontor-go/badges/master/pipeline.svg)](https://gitlab.thpeetz.de/kontor/kontor-go/commits/master) +[![coverage report](https://gitlab.thpeetz.de/kontor/kontor-go/badges/master/coverage.svg)](https://gitlab.thpeetz.de/kontor/kontor-go/commits/master) + +# Kontor diff --git a/go/cmd/kontor/main.go b/go/cmd/kontor/main.go new file mode 100644 index 0000000..c2dbeeb --- /dev/null +++ b/go/cmd/kontor/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "database/sql" + "log" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +func main() { + connectionString := "kontor:kontor@tcp(127.0.0.1:3306)/kontor?parseTime=true" + db, err := sql.Open("mysql", connectionString) + if err != nil { + log.Printf("setup database error: %v", err) + return + } + db.SetConnMaxLifetime(time.Minute * 3) + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(10) + log.Println("Database connected") + + rows, err := db.Query("SELECT id, created_date, last_modified_date, version, url, title, review FROM media_file") + if err != nil { + log.Fatal((err.Error())) + } + defer rows.Close() + var rec MediaFile + for rows.Next() { + err = rows.Scan(&rec.ID, &rec.CreatedDate, &rec.LastModifiedDate, &rec.Version, &rec.Url, &rec.Title, &rec.Review) + if err != nil { + log.Fatal(err.Error()) + } + log.Println(rec, string(rec.Url), string(rec.Title)) + } +} diff --git a/go/cmd/kontor/model.go b/go/cmd/kontor/model.go new file mode 100644 index 0000000..b7634c5 --- /dev/null +++ b/go/cmd/kontor/model.go @@ -0,0 +1,25 @@ +package main + +import ( + "time" + + "github.com/google/uuid" +) + +type AbstractEntity struct { + ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"` + Version uint `json:"version"` + CreatedDate time.Time `json:"createdDate"` + LastModifiedDate time.Time `json:"lastModifiedDate"` +} + +type MediaFile struct { + AbstractEntity + Url []byte `json:"url"` + Review []uint8 `json:"review"` + ShouldDownload []uint8 `json:"shouldDownload"` + Title []byte `json:"title"` + CloudLink string `json:"cloudLink"` + FileName string `json:"fileName"` + Path string `json:"path"` +} diff --git a/go/cmd/root.go b/go/cmd/root.go new file mode 100644 index 0000000..9a1c5e2 --- /dev/null +++ b/go/cmd/root.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + "log" + "os" + + "gitlab.thpeetz.de/kontor/kontor-go/pkg/properties" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/setup" + + "github.com/gin-gonic/gin" + "github.com/spf13/cobra" +) + +var ( + // Verbose defines the parameter verbose. + Verbose bool + // Version defines the version of the web application. + Version string + // Debug defines the debug parameter. + Debug bool + // Port defines the parameter port. + Port int + // TemplatesDir defines the root directory of template files. + TemplatesDir string + router *gin.Engine +) + +var rootCmd = &cobra.Command{ + Use: "kontor", + Short: "kontor", + Long: `kontor`, + Run: func(cmd *cobra.Command, args []string) { + // Set Gin to production mode + if Debug { + gin.SetMode(gin.DebugMode) + } else { + gin.SetMode(gin.ReleaseMode) + } + log.SetOutput(gin.DefaultWriter) + + // Set the router as the default one provided by Gin + router = gin.Default() + + // Process the templates at the start so that they don't have to be loaded + // from the disk again. This makes serving HTML pages very fast. + templatesDir := fmt.Sprintf("%s/**/*", TemplatesDir) + log.Printf("load template files from %v", templatesDir) + router.LoadHTMLGlob(templatesDir) + + // Use Middleware logger + router.Use(gin.Logger()) + + // Initialize the routes + setup.InitializeRoutes(router) + + // Clean up SetSessionStatus + setup.CleanupSessions() + + // Check if at least one user configured + setup.CheckUserList() + + // Check if data for TradeYourSportsCards is available + setup.CheckTradeYourSportsCardsData() + + //properties.SetVersion(Version) + + // Start serving the application + server := fmt.Sprintf("127.0.0.1:%d", Port) + router.Run(server) + }, +} + +// Execute parses the commandline and calls Execute on rootCmd. +func Execute(version string) { + rootCmd.Version = version + properties.SetVersion(version) + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output") + rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "debug modus") + rootCmd.PersistentFlags().IntVarP(&Port, "port", "p", 8500, "port number") + rootCmd.PersistentFlags().StringVarP(&TemplatesDir, "templates", "t", "templates", "path for template files") +} diff --git a/go/cmd/server.go b/go/cmd/server.go new file mode 100644 index 0000000..3b7ebd0 --- /dev/null +++ b/go/cmd/server.go @@ -0,0 +1,32 @@ +package main + +import ( + "github.com/joho/godotenv" + application "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/app" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/kernel" +) + +// init is invoked before main() +func init() { + // loads values from .env into the system + if err := godotenv.Load(); err != nil { + panic("No .env file found") + } +} + +func main() { + // Create our application + app := kernel.Boot() + + // Build our services + //ping.BuildPingService(app) + + // Run our Application in a coroutine + go func() { + app.Run() + }() + + // Wait for termination signals and shut down gracefully + application.WaitForShutdown(app) + +} diff --git a/go/comics.xml b/go/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/go/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/go/docs/build.gradle b/go/docs/build.gradle new file mode 100644 index 0000000..9f27a44 --- /dev/null +++ b/go/docs/build.gradle @@ -0,0 +1,7 @@ +plugins { + alias(versionsLibs.plugins.asciidoctorConvention) +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/go/docs/gradle.properties b/go/docs/gradle.properties new file mode 100644 index 0000000..aeb4e86 --- /dev/null +++ b/go/docs/gradle.properties @@ -0,0 +1,2 @@ +description='Documentation for application kontor-go' +version=1.0.0-SNAPSHOT diff --git a/go/docs/gradle/wrapper/gradle-wrapper.jar b/go/docs/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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/go/docs/gradlew.bat b/go/docs/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/go/docs/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% 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/go/docs/settings.gradle b/go/docs/settings.gradle new file mode 100644 index 0000000..b956775 --- /dev/null +++ b/go/docs/settings.gradle @@ -0,0 +1 @@ +rootProject.name='kontor-go' diff --git a/go/docs/src/docs/asciidoc/kontor-go.adoc b/go/docs/src/docs/asciidoc/kontor-go.adoc new file mode 100644 index 0000000..5334cfb --- /dev/null +++ b/go/docs/src/docs/asciidoc/kontor-go.adoc @@ -0,0 +1,31 @@ += Projektbeschreibung Kontor +:author: Thomas Peetz +:email: +:doctype: article +:toc: left +:sectnums: + +== Einführung + +=== Zweck + +== Anforderungen + +== Implementierung + +=== Datenmodell + +== Betrieb + +[bibliography] +== Referenzen + +- [[[1]]] Thomas Peetz, Betriebshandbuch IBTP + +[index] +== Index + +== Tabellenverzeichnis + +[glossary] +== Glossar diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..cc805de --- /dev/null +++ b/go/go.mod @@ -0,0 +1,28 @@ +module gitlab.com/tpeetz-kontor/kontor-go + +go 1.22.2 + +require github.com/joho/godotenv v1.5.1 + +require ( + github.com/felixge/httpsnoop v1.0.3 // indirect + go.uber.org/multierr v1.10.0 // indirect +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 + github.com/gorilla/handlers v1.5.2 + github.com/gorilla/mux v1.8.1 + github.com/jinzhu/gorm v1.9.16 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + go.uber.org/zap v1.27.0 + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/gorm v1.25.12 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..3d35401 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,62 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 h1:SHq4Rl+B7WvyM4XODon1LXtP7gcG49+7Jubt1gWWswY= +golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3/go.mod h1:bqv7PJ/TtlrzgJKhOAGdDUkUltQapRik/UEHubLVBWo= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= +honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= diff --git a/go/kontor-ansible.yml b/go/kontor-ansible.yml new file mode 100644 index 0000000..2e19328 --- /dev/null +++ b/go/kontor-ansible.yml @@ -0,0 +1,58 @@ +--- +# file: kontor-ansible.yml +- hosts: localhost + remote_user: root + tasks: + - name: create group kontor + group: + name: kontor + state: present + + - name: create user kontor + user: + name: kontor + home: /home/kontor + shell: /bin/bash + state: present + # generate_ssh_key: yes + # ssh_key_bits: 4096 + # ssh_key_file: .ssh/id_rsa + + - name: create log directory + file: + path: /var/log/kontor + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: true + + - name: create run directory + file: + path: /var/run/kontor + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: true + + - name: create directory + file: + path: /home/kontor/production + state: directory + owner: kontor + group: kontor + mode: 0775 + recurse: false + + - name: copy binary + copy: + src: bin/kontor + dest: /home/kontor/production/kontor + mode: 0775 + + - name: copy templates + copy: + src: templates + dest: /home/kontor/production/kontor/templates + mode: 0775 diff --git a/go/pkg/admin/routes.go b/go/pkg/admin/routes.go new file mode 100644 index 0000000..a4845cf --- /dev/null +++ b/go/pkg/admin/routes.go @@ -0,0 +1,23 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes sets the routes for the administrative data urls. +func GetRoutes(router *gin.Engine) { + adminRoutes := router.Group("/admin") + { + adminRoutes.GET("/", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showAdminIndex) + adminRoutes.GET("/user", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserIndex) + adminRoutes.POST("/user", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserIndex) + adminRoutes.GET("/user/view/:userid", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserDetails) + adminRoutes.POST("/user/view", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateUserCreation) + adminRoutes.GET("/user/create", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showUserCreation) + adminRoutes.POST("/user/create", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateUserCreation) + adminRoutes.GET("/data", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), showDataUpload) + adminRoutes.POST("/data", auth.EnsureLoggedIn(), auth.EnsureAdminStatus(), validateDataUpload) + } +} diff --git a/go/pkg/admin/user.go b/go/pkg/admin/user.go new file mode 100644 index 0000000..8d94a06 --- /dev/null +++ b/go/pkg/admin/user.go @@ -0,0 +1,18 @@ +package admin + +import ( + "gopkg.in/mgo.v2/bson" +) + +// User defines the data model for application users with id,email, user name, +// first and family name, password and admin status. +type User struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Email string `json:"email" bson:"email,omitempty"` + Username string `json:"username" bson:"username,omitempty"` + Firstname string `json:"firstname" bson:"firstname,omitempty"` + Lastname string `json:"lastname" bson:"lastname,omitempty"` + Password string `json:"password" bson:"password,omitempty"` + IsAdmin bool `json:"is_admin" bson:"is_admin,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/admin/user_dao.go b/go/pkg/admin/user_dao.go new file mode 100644 index 0000000..3f77fbd --- /dev/null +++ b/go/pkg/admin/user_dao.go @@ -0,0 +1,123 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "log" + + "github.com/gin-gonic/gin" + + "golang.org/x/crypto/bcrypt" + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// UserDAO extends the type BaseDAO. +type UserDAO struct { + Db dao.BaseDAO +} + +const ( + // USERCOLLECTION defines the collection name for storing application user data. + USERCOLLECTION = "user" + // USERMODEL defines the name of the user data model. + USERMODEL = "kontor.admin.user" +) + +// HashPassword returns the encrypted password from password string. +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +// CheckPasswordHash returns if password correlates with pasword hash. +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// FindAll retrieves the list of users from the database. +func (m *UserDAO) FindAll() ([]User, error) { + m.Db.Connect() + var users []User + err := m.Db.MongoDb.C(USERCOLLECTION).Find(bson.M{"model": USERMODEL}).All(&users) + return users, err +} + +// FindByID returns a user with given id or returns the error. +func (m *UserDAO) FindByID(id string) (User, error) { + m.Db.Connect() + var user User + err := m.Db.MongoDb.C(USERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&user) + return user, err +} + +// FindByUsername returns a user with given name or returns the error. +func (m *UserDAO) FindByUsername(username string) (User, error) { + m.Db.Connect() + var user User + err := m.Db.MongoDb.C(USERCOLLECTION).Find(bson.M{"username": username, "model": USERMODEL}).One(&user) + return user, err +} + +// Insert a user into database. +func (m *UserDAO) Insert(user User) error { + m.Db.Connect() + user.Model = USERMODEL + err := m.Db.MongoDb.C(USERCOLLECTION).Insert(&user) + return err +} + +// Upsert a user into database. +func (m *UserDAO) Upsert(user User) (*mgo.ChangeInfo, error) { + m.Db.Connect() + user.Model = USERMODEL + info, err := m.Db.MongoDb.C(USERCOLLECTION).Upsert(bson.M{"username": user.Username}, &user) + return info, err +} + +// Update an existing user. +func (m *UserDAO) Update(user User) error { + m.Db.Connect() + err := m.Db.MongoDb.C(USERCOLLECTION).UpdateId(user.ID, &user) + return err +} + +// Delete an existing user. +func (m *UserDAO) Delete(user User) error { + m.Db.Connect() + err := m.Db.MongoDb.C(USERCOLLECTION).Remove(&user) + return err +} + +// IsUserValid checks if the username and password combination is valid +func (m *UserDAO) IsUserValid(username, password string) bool { + if gin.IsDebugging() { + log.Printf("UserDAO.IsUserValid(%s)", username) + } + user, err := m.FindByUsername(username) + if gin.IsDebugging() { + log.Printf("UserDAO.IsUserValid: %v, %v", user, err) + } + if &user == nil || err != nil { + return false + } + return CheckPasswordHash(password, user.Password) +} + +// IsUserAdmin checks if user identified by name has admin rights. +func (m *UserDAO) IsUserAdmin(username string) bool { + user, err := m.FindByUsername(username) + if &user == nil || err != nil { + return false + } + return user.IsAdmin +} + +// IsUsernameAvailable checks if the supplied username is available. +func (m *UserDAO) IsUsernameAvailable(username string) bool { + user, err := m.FindByUsername(username) + if &user == nil || err != nil { + return true + } + return false +} diff --git a/go/pkg/admin/user_test.go b/go/pkg/admin/user_test.go new file mode 100644 index 0000000..0668ae2 --- /dev/null +++ b/go/pkg/admin/user_test.go @@ -0,0 +1,105 @@ +package admin + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var userModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Email", "string"}, + {"Username", "string"}, + {"Firstname", "string"}, + {"Lastname", "string"}, + {"Password", "string"}, + {"IsAdmin", "bool"}, + {"Model", "string"}, +} + +func TestUserModel(t *testing.T) { + m := User{} + if reflect.TypeOf(m).NumField() != len(userModelTestTable) { + t.Fail() + } + for index, testData := range userModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListUsers(t *testing.T) { + var userDao = UserDAO{Db: dao.TestDb} + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + if users != nil { + t.Fail() + } +} + +func TestInsertUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + user = User{} + users []User + ) + user.ID = bson.NewObjectId() + user.Username = "test" + err := userDao.Insert(user) + if err != nil { + t.Fail() + } + users, err = userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 1 { + t.Fail() + } +} + +func TestUpsertUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + user = User{} + ) + user.ID = bson.NewObjectId() + user.Username = "test2" + userDao.Upsert(user) + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 2 { + t.Fail() + } +} + +func TestDeleteUser(t *testing.T) { + var ( + userDao = UserDAO{Db: dao.TestDb} + ) + users, err := userDao.FindAll() + if err != nil { + t.Fail() + } + for _, user := range users { + userDao.Delete(user) + } + users, err = userDao.FindAll() + if err != nil { + t.Fail() + } + if len(users) != 0 { + t.Fail() + } +} diff --git a/go/pkg/admin/views.go b/go/pkg/admin/views.go new file mode 100644 index 0000000..f1b62cc --- /dev/null +++ b/go/pkg/admin/views.go @@ -0,0 +1,242 @@ +package admin + +import ( + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/comics" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" + "github.com/smallfish/simpleyaml" +) + +const ( + // KontorUserAdministrationTitle defines the text of the page title + KontorUserAdministrationTitle = "Kontor User Administration" + // DataUploadTemplate defines the name of the template file for the data upload + DataUploadTemplate = "kontor/data-upload.html" +) + +// ShowLoginPage renders login page. +func ShowLoginPage(c *gin.Context) { + // Call the render function with the name of the template to render + util.Render(c, gin.H{"title": "Login"}, "login.html") +} + +// PerformLogin reads data from login form and validates input. +func PerformLogin(c *gin.Context) { + // Obtain the POSTed username and password values + username := c.PostForm("username") + password := c.PostForm("password") + + var userDao = UserDAO{Db: dao.KontorDb} + + // Check if the username/password combination is valid + if userDao.IsUserValid(username, password) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + user, _ := userDao.FindByUsername(username) + sessionDao := auth.SessionDAO{Db: dao.KontorDb} + session, _ := sessionDao.FindByID(sessionID) + session.Username = username + session.IsAdmin = user.IsAdmin + sessionDao.Update(session) + util.Render(c, gin.H{"title": "Successful Login", "InfoMessage": "Login successfull"}, "kontor/index.html") + } else { + // If the username/password combination is invalid, + // show the error message on the login page + c.HTML(http.StatusBadRequest, "login.html", gin.H{ + "ErrorTitle": "Login Failed", + "ErrorMessage": "Invalid credentials provided"}) + } +} + +// Logout invalidates session. +func Logout(c *gin.Context) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + c.SetCookie("session", sessionID, -1, "", "", false, true) + + // Redirect to the home page + c.Redirect(http.StatusTemporaryRedirect, "/") +} + +func showAdminIndex(c *gin.Context) { + // Call the render function with the name of the template to render + util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") +} + +func showUserIndex(c *gin.Context) { + var dao = UserDAO{Db: dao.KontorDb} + if users, err := dao.FindAll(); err == nil && users != nil { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": users}, "kontor/users.html") + } else { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": users, "ErrorMessage": err}, "kontor/users.html") + } +} + +func showUserDetails(c *gin.Context) { + userID := c.Param("userid") + var userDao = UserDAO{Db: dao.KontorDb} + if user, err := userDao.FindByID(userID); err == nil && &user != nil { + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": user, "action": util.SaveAction}, "kontor/user-detail.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showUserCreation(c *gin.Context) { + var user = User{} + util.Render(c, gin.H{"title": KontorUserAdministrationTitle, "payload": user, "action": util.AddAction}, "kontor/user-detail.html") +} + +func validateUserCreation(c *gin.Context) { + // Obtain the POSTed username and password values + username := c.PostForm("username") + firstname := c.PostForm("firstname") + lastname := c.PostForm("lastname") + password := c.PostForm("password") + adminFormVar := c.PostForm("admin") + action := c.PostForm("action") + userid := c.PostForm("userid") + isAdmin, _ := strconv.ParseBool(adminFormVar) + + var err error + var dao = UserDAO{Db: dao.KontorDb} + var user = User{} + + switch action { + case util.AddAction: + user.Username = username + user.Firstname = firstname + user.Lastname = lastname + user.IsAdmin = isAdmin + user.Password, _ = HashPassword(password) + _, err = dao.Upsert(user) + case util.SaveAction: + user, _ = dao.FindByID(userid) + user.Username = username + user.Firstname = firstname + user.Lastname = lastname + user.IsAdmin = isAdmin + user.Password, _ = HashPassword(password) + err = dao.Update(user) + case util.DeleteAction: + user, _ = dao.FindByID(userid) + err = dao.Delete(user) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/admin/user") + } else { + c.HTML(http.StatusBadRequest, "kontor/create-user.html", gin.H{ + "ErrorTitle": "User Creation Failed", + "ErrorMessage": err.Error()}) + } +} + +func showDataUpload(c *gin.Context) { + // Call the render function with the name of the template to render + //util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") + util.Render(c, gin.H{"title": "Kontor Data Upload", "payload": nil}, DataUploadTemplate) +} + +func validateDataUpload(c *gin.Context) { + // Call the render function with the name of the template to render + //util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/admin.html") + datafile, err := c.FormFile("datafile") + if err != nil { + c.HTML(http.StatusBadRequest, DataUploadTemplate, gin.H{ + "ErrorTitle": "Data Upload Failed", + "ErrorMessage": err.Error()}) + return + } + log.Printf("Data File: %v", datafile.Filename) + filename := filepath.Base(datafile.Filename) + if err := c.SaveUploadedFile(datafile, filename); err != nil { + c.HTML(http.StatusBadRequest, DataUploadTemplate, gin.H{ + "ErrorTitle": "Data Upload Failed", + "ErrorMessage": err.Error()}) + return + } + source, err := ioutil.ReadFile(filename) + if err != nil { + panic(err) + } + yaml, err := simpleyaml.NewYaml(source) + if err != nil { + panic(err) + } + if yaml.IsArray() { + size, err := yaml.GetArraySize() + if err != nil { + panic(err) + } + log.Printf("Found %d entries.\n", size) + var publisherDao comics.PublisherDAO + publisherDao.Db = dao.KontorDb + var artistDao comics.ArtistDAO + artistDao.Db = dao.KontorDb + var comicDao comics.ComicDAO + comicDao.Db = dao.KontorDb + publisherMap := make(map[int]string) + for index := 0; index < size; index++ { + entry := yaml.GetIndex(index) + if entry.IsMap() { + model, err := entry.Get("model").String() + if err != nil { + panic(err) + } + pk, _ := entry.Get("pk").Int() + switch model { + case "comics.publisher": + name, err := entry.Get("fields").Get("name").String() + if err != nil { + panic(err) + } + log.Printf(" %v %v %v\n", pk, model, name) + publisherMap[pk] = name + publisher := comics.Publisher{} + publisher.Name = name + info, _ := publisherDao.Upsert(publisher) + log.Printf("Publisher records changed: %d", info.Updated) + case "comics.artist": + name, err := entry.Get("fields").Get("name").String() + if err != nil { + panic(err) + } + log.Printf(" %v %v %v\n", pk, model, name) + artist := comics.Artist{} + artist.Name = name + info, _ := artistDao.Upsert(artist) + log.Printf("Artist records changed: %d", info.Updated) + case "comics.comic": + title, err := entry.Get("fields").Get("title").String() + if err != nil { + panic(err) + } + publisherID, err := entry.Get("fields").Get("publisher").Int() + publisher, err := publisherDao.FindByName(publisherMap[publisherID]) + completed, err := entry.Get("fields").Get("completed").Bool() + if err != nil { + log.Printf("Error occured: %v", err) + } + log.Printf(" %v %v %v %v\n", pk, model, title, completed) + comic := comics.Comic{} + comic.Title = title + comic.Completed = completed + comic.Publisher = publisher.ID + info, _ := comicDao.Upsert(comic) + log.Printf("Comic records changed: %d", info.Updated) + } + //fmt.Printf("Entry %d: %v\n", index, entry) + } + } + } + util.Render(c, gin.H{"title": "Kontor Data Upload", "payload": nil}, DataUploadTemplate) +} diff --git a/go/pkg/application/registry/comic/registry.go b/go/pkg/application/registry/comic/registry.go new file mode 100644 index 0000000..7fb2f95 --- /dev/null +++ b/go/pkg/application/registry/comic/registry.go @@ -0,0 +1,19 @@ +package comic + +import ( + "net/http" + + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/context/comic/routing" + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/app" +) + +func BuildComicService(app *app.Application) { + // Create our Handler + handler := routing.NewHandler(app) + + // Create a sub router for this service + router := app.Router.Methods(http.MethodGet).Subrouter() + + // Register our service routes + router.HandleFunc("/comics/comic", handler.ComicList).Name("comics:comicList") +} diff --git a/go/pkg/auth/middleware.go b/go/pkg/auth/middleware.go new file mode 100644 index 0000000..a26a93d --- /dev/null +++ b/go/pkg/auth/middleware.go @@ -0,0 +1,87 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/properties" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "gopkg.in/mgo.v2/bson" +) + +var sessionDao = SessionDAO{Db: dao.KontorDb} + +// EnsureLoggedIn ensures that a request will be aborted with an error +// if the user is not logged in +func EnsureLoggedIn() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's an error or if the token is empty + // the user is not logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || session.Username == "" { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// EnsureAdminStatus ensures that a request will be aborted with an error +// if the user is not logged in +func EnsureAdminStatus() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's an error or if the token is empty + // the user is not logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || !session.IsAdmin { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// EnsureNotLoggedIn ensures that a request will be aborted with an error +// if the user is already logged in +func EnsureNotLoggedIn() gin.HandlerFunc { + return func(c *gin.Context) { + // If there's no error or if the token is not empty + // the user is already logged in + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + if session, err := sessionDao.GetSession(sessionID); err != nil || session.Username != "" { + c.Redirect(http.StatusTemporaryRedirect, "/") + //c.AbortWithStatus(http.StatusUnauthorized) + } + } +} + +// SetSessionStatus reads sessionId from cookie if available or create new session object +// and sets cookie accordingly. +func SetSessionStatus() gin.HandlerFunc { + return func(c *gin.Context) { + if sessionID, err := c.Cookie("session"); err == nil || sessionID != "" { + c.Set("session", sessionID) + } else { + session, _ := sessionDao.GetSession(bson.NewObjectId().Hex()) + sessionID := session.ID.Hex() + c.Set("session", sessionID) + c.SetCookie("session", sessionID, 3600, "", "", false, true) + } + } +} + +// SetSessionData populates session information with username, admin status of user and +// application version. +func SetSessionData(c *gin.Context, data gin.H) { + sessionInterface, _ := c.Get("session") + sessionID := sessionInterface.(string) + session, _ := sessionDao.GetSession(sessionID) + if gin.IsDebugging() { + log.Printf("setSessionData(%v): %v", sessionID, session) + } + data["is_logged_in"] = (session.Username != "") + data["is_admin"] = session.IsAdmin + data["version"] = properties.Version +} diff --git a/go/pkg/auth/session.go b/go/pkg/auth/session.go new file mode 100644 index 0000000..dccf9fb --- /dev/null +++ b/go/pkg/auth/session.go @@ -0,0 +1,11 @@ +package auth + +import "gopkg.in/mgo.v2/bson" + +// Session defines the data model for sessions with id,user name and admin status. +type Session struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Username string `json:"username" bson:"username,omitempty"` + IsAdmin bool `json:"is_admin" bson:"is_admin,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/auth/session_dao.go b/go/pkg/auth/session_dao.go new file mode 100644 index 0000000..70f7adc --- /dev/null +++ b/go/pkg/auth/session_dao.go @@ -0,0 +1,78 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// SessionDAO extends the type BaseDAO. +type SessionDAO struct { + Db dao.BaseDAO +} + +const ( + // SESSIONCOLLECTION defines the collection name for storing session data. + SESSIONCOLLECTION = "session" + // SESSIONMODEL defines the name of the session data model. + SESSIONMODEL = "kontor.admin.session" +) + +// FindAll retrieves the list of sessions from the database. +func (m *SessionDAO) FindAll() ([]Session, error) { + m.Db.Connect() + var sessions []Session + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Find(bson.M{"model": SESSIONMODEL}).All(&sessions) + return sessions, err +} + +// FindByID returns a session with given id or returns the error. +func (m *SessionDAO) FindByID(id string) (Session, error) { + m.Db.Connect() + var session Session + err := m.Db.MongoDb.C(SESSIONCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&session) + return session, err +} + +// Insert a session into database. +func (m *SessionDAO) Insert(session Session) error { + m.Db.Connect() + session.Model = SESSIONMODEL + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Insert(&session) + //log.PrintDebug("Insert: %v, %v\n", session, err) + return err +} + +// Upsert a session into database. +func (m *SessionDAO) Upsert(session Session) (*mgo.ChangeInfo, error) { + m.Db.Connect() + session.Model = SESSIONMODEL + info, err := m.Db.MongoDb.C(SESSIONCOLLECTION).Upsert(bson.M{"_id": session.ID}, &session) + return info, err +} + +// Update an existing session. +func (m *SessionDAO) Update(session Session) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SESSIONCOLLECTION).UpdateId(session.ID, &session) + return err +} + +// Delete an existing session. +func (m *SessionDAO) Delete(session Session) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SESSIONCOLLECTION).Remove(&session) + return err +} + +// GetSession get a session by given id or create a new one, if nothing was found. +func (m *SessionDAO) GetSession(id string) (*Session, error) { + m.Db.Connect() + session, err := m.FindByID(id) + if err != nil { + session = Session{ID: bson.ObjectIdHex(id), Username: "", IsAdmin: false} + m.Insert(session) + } + return &session, nil +} diff --git a/go/pkg/auth/session_test.go b/go/pkg/auth/session_test.go new file mode 100644 index 0000000..a6769b8 --- /dev/null +++ b/go/pkg/auth/session_test.go @@ -0,0 +1,103 @@ +package auth + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var sessionModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Username", "string"}, + {"IsAdmin", "bool"}, + {"Model", "string"}, +} + +func TestSessionModel(t *testing.T) { + m := Session{} + if reflect.TypeOf(m).NumField() != len(sessionModelTestTable) { + t.Fail() + } + for index, testData := range sessionModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListSessions(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + ) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + if sessions != nil { + t.Fail() + } +} + +func TestInsertSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + session = Session{} + sessions []Session + ) + session.ID = bson.NewObjectId() + session.Username = "test" + err := sessionDao.Insert(session) + if err != nil { + t.Fail() + } + sessions, err = sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 1 { + t.Fail() + } +} + +func TestUpsertSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + session = Session{} + ) + session.ID = bson.NewObjectId() + session.Username = "test2" + sessionDao.Upsert(session) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 2 { + t.Fail() + } +} + +func TestDeleteSession(t *testing.T) { + var ( + sessionDao = SessionDAO{Db: dao.TestDb} + ) + sessions, err := sessionDao.FindAll() + if err != nil { + t.Fail() + } + for _, session := range sessions { + sessionDao.Delete(session) + } + sessions, err = sessionDao.FindAll() + if err != nil { + t.Fail() + } + if len(sessions) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/artist.go b/go/pkg/comics/artist.go new file mode 100644 index 0000000..8afd80a --- /dev/null +++ b/go/pkg/comics/artist.go @@ -0,0 +1,12 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Artist defines the data model for comic artists with id and name. +type Artist struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/artist_dao.go b/go/pkg/comics/artist_dao.go new file mode 100644 index 0000000..b177c2b --- /dev/null +++ b/go/pkg/comics/artist_dao.go @@ -0,0 +1,78 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "log" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ArtistDAO extends the type BaseDAO. +type ArtistDAO struct { + Db dao.BaseDAO +} + +const ( + // ARTISTCOLLECTION defines the collection name for storing comic artists. + ARTISTCOLLECTION = "artist" + // ARTISTMODEL defines the name of the artist data model. + ARTISTMODEL = "kontor.comics.artist" +) + +// FindAll retrieves the list of artists from the database. +func (m *ArtistDAO) FindAll() ([]Artist, error) { + m.Db.Connect() + var artists []Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Find(bson.M{"model": ARTISTMODEL}).All(&artists) + return artists, err +} + +// FindByID returns an artists with given id or returns the error. +func (m *ArtistDAO) FindByID(id string) (Artist, error) { + m.Db.Connect() + var artist Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&artist) + return artist, err +} + +// FindByName returns an artists with given name or returns the error. +func (m *ArtistDAO) FindByName(name string) (Artist, error) { + m.Db.Connect() + var artist Artist + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Find(bson.M{"name": name, "model": ARTISTMODEL}).One(&artist) + return artist, err +} + +// Insert an artist into database. +func (m *ArtistDAO) Insert(artist Artist) error { + m.Db.Connect() + artist.Model = ARTISTMODEL + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Insert(&artist) + return err +} + +// Upsert an artist into database. +func (m *ArtistDAO) Upsert(artist Artist) (*mgo.ChangeInfo, error) { + m.Db.Connect() + artist.Model = ARTISTMODEL + info, err := m.Db.MongoDb.C(ARTISTCOLLECTION).Upsert(bson.M{"name": artist.Name}, &artist) + return info, err +} + +// Delete an existing artist. +func (m *ArtistDAO) Delete(artist Artist) error { + m.Db.Connect() + err := m.Db.MongoDb.C(ARTISTCOLLECTION).Remove(&artist) + if err != nil { + log.Printf("ArtistDao.Delete: %v", err) + } + return err +} + +// Update an existing artist. +func (m *ArtistDAO) Update(artist Artist) error { + m.Db.Connect() + err := m.Db.MongoDb.C(ARTISTCOLLECTION).UpdateId(artist.ID, &artist) + return err +} diff --git a/go/pkg/comics/artist_test.go b/go/pkg/comics/artist_test.go new file mode 100644 index 0000000..96e23ca --- /dev/null +++ b/go/pkg/comics/artist_test.go @@ -0,0 +1,96 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var artistModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestArtistModel(t *testing.T) { + m := Artist{} + if reflect.TypeOf(m).NumField() != len(artistModelTestTable) { + t.Fail() + } + for index, testData := range artistModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListArtists(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + ) + artists, err := artistDao.FindAll() + if err != nil { + t.Fail() + } + if artists != nil { + t.Fail() + } +} + +func TestInsertArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + artist = Artist{} + artists []Artist + ) + artist.ID = bson.NewObjectId() + artist.Name = "Turner, Michael" + err := artistDao.Insert(artist) + if err != nil { + t.Fail() + } + artists, _ = artistDao.FindAll() + if len(artists) != 1 { + t.Fail() + } +} + +func TestUpsertArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + artist = Artist{} + ) + artist.ID = bson.NewObjectId() + artist.Name = "Marz, Ron" + _, err := artistDao.Upsert(artist) + if err != nil { + t.Fail() + } + artists, _ := artistDao.FindAll() + if len(artists) != 2 { + t.Fail() + } +} + +func TestDeleteArtist(t *testing.T) { + var ( + artistDao = ArtistDAO{Db: dao.TestDb} + ) + artists, err := artistDao.FindAll() + if err != nil { + t.Fail() + } + for _, artist := range artists { + artistDao.Delete(artist) + } + artists, _ = artistDao.FindAll() + if len(artists) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/artist_views.go b/go/pkg/comics/artist_views.go new file mode 100644 index 0000000..c685f1b --- /dev/null +++ b/go/pkg/comics/artist_views.go @@ -0,0 +1,23 @@ +package comics + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" +) + +const ( + // ArtistPublisherTemplate defines name of template file for comics publishers + ArtistPublisherTemplate = "comics/publishers.html" +) + +func showArtistList(c *gin.Context) { + var dao = ArtistDAO{Db: dao.KontorDb} + if artists, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comic Artists", "payload": artists}, "artists.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/comics/comic.go b/go/pkg/comics/comic.go new file mode 100644 index 0000000..7332d65 --- /dev/null +++ b/go/pkg/comics/comic.go @@ -0,0 +1,15 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Comic defines the data model for comic issues with id, title, publisher, order and completion status. +type Comic struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Publisher bson.ObjectId `json:"publisher" bson:"publisher,omitempty"` + CurrentOrder bool `json:"current_order" bson:"current_order"` + Completed bool `json:"completed" bson:"completed"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/comic_dao.go b/go/pkg/comics/comic_dao.go new file mode 100644 index 0000000..c9f9b02 --- /dev/null +++ b/go/pkg/comics/comic_dao.go @@ -0,0 +1,75 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ComicDAO extends the type BaseDAO. +type ComicDAO struct { + Db dao.BaseDAO +} + +const ( + // COMICCOLLECTION defines the collection name for storing comics. + COMICCOLLECTION = "comic" + // COMICMODEL defines the name of the comic data model. + COMICMODEL = "kontor.comics.comic" +) + +// FindAll retrieves the list of comisc from the database. +func (m *ComicDAO) FindAll() ([]Comic, error) { + m.Db.Connect() + var comics []Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).Find(bson.M{"model": COMICMODEL}).All(&comics) + return comics, err +} + +// FindByID returns an comic with given id or returns the error. +func (m *ComicDAO) FindByID(id string) (Comic, error) { + m.Db.Connect() + var comic Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&comic) + return comic, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *ComicDAO) FindByName(name string) (Comic, error) { + m.Db.Connect() + var comic Comic + err := m.Db.MongoDb.C(COMICCOLLECTION).Find(bson.M{"name": name, "model": COMICMODEL}).One(&comic) + return comic, err +} + +// Insert a comic into database. +func (m *ComicDAO) Insert(comic Comic) error { + m.Db.Connect() + comic.Model = COMICMODEL + err := m.Db.MongoDb.C(COMICCOLLECTION).Insert(&comic) + //util.PrintDebug("ComicDAO.Insert: %v", comic) + return err +} + +// Upsert a comic into database. +func (m *ComicDAO) Upsert(comic Comic) (*mgo.ChangeInfo, error) { + m.Db.Connect() + comic.Model = COMICMODEL + info, err := m.Db.MongoDb.C(COMICCOLLECTION).Upsert(bson.M{"title": comic.Title}, &comic) + return info, err +} + +// Delete an existing comic. +func (m *ComicDAO) Delete(comic Comic) error { + m.Db.Connect() + err := m.Db.MongoDb.C(COMICCOLLECTION).Remove(&comic) + return err +} + +// Update an existing movie +func (m *ComicDAO) Update(comic Comic) error { + m.Db.Connect() + err := m.Db.MongoDb.C(COMICCOLLECTION).UpdateId(comic.ID, &comic) + return err +} diff --git a/go/pkg/comics/comic_test.go b/go/pkg/comics/comic_test.go new file mode 100644 index 0000000..baea792 --- /dev/null +++ b/go/pkg/comics/comic_test.go @@ -0,0 +1,88 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var comicModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Title", "string"}, + {"Publisher", "string"}, + {"CurrentOrder", "bool"}, + {"Completed", "bool"}, + {"Model", "string"}, +} + +func TestComicModel(t *testing.T) { + m := Comic{} + if reflect.TypeOf(m).NumField() != len(comicModelTestTable) { + t.Fail() + } + for index, testData := range comicModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListComics(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + ) + comics, err := comicDao.FindAll() + if err != nil { + t.Fail() + } + if len(comics) != 0 { + t.Fail() + } +} + +func TestInsertComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + comic = Comic{} + comics []Comic + ) + comic.ID = bson.NewObjectId() + comic.Title = "Simpsons" + comicDao.Insert(comic) + comics, _ = comicDao.FindAll() + if len(comics) != 1 { + t.Fail() + } +} +func TestUpsertComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + comic = Comic{} + ) + comic.ID = bson.NewObjectId() + comic.Title = "1602" + comicDao.Upsert(comic) + comics, _ := comicDao.FindAll() + if len(comics) != 2 { + t.Fail() + } +} +func TestDeleteComic(t *testing.T) { + var ( + comicDao = ComicDAO{Db: dao.TestDb} + ) + comics, _ := comicDao.FindAll() + for _, comic := range comics { + comicDao.Delete(comic) + } + comics, _ = comicDao.FindAll() + if len(comics) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/publisher.go b/go/pkg/comics/publisher.go new file mode 100644 index 0000000..7bef875 --- /dev/null +++ b/go/pkg/comics/publisher.go @@ -0,0 +1,12 @@ +package comics + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Publisher defines the data model for comic publishers with id and name. +type Publisher struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/comics/publisher_dao.go b/go/pkg/comics/publisher_dao.go new file mode 100644 index 0000000..f12cf22 --- /dev/null +++ b/go/pkg/comics/publisher_dao.go @@ -0,0 +1,75 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PublisherDAO extends the type BaseDAO. +type PublisherDAO struct { + Db dao.BaseDAO +} + +const ( + // PUBLISHERCOLLECTION defines the collection name for storing publishers. + PUBLISHERCOLLECTION = "publisher" + // PUBLISHERMODEL defines the name of the publisher data model. + PUBLISHERMODEL = "kontor.comics.publisher" +) + +// FindAll retrieves the list of publishers from the database. +func (m *PublisherDAO) FindAll() ([]Publisher, error) { + m.Db.Connect() + var publishers []Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"model": PUBLISHERMODEL}).All(&publishers) + return publishers, err +} + +// FindByID returns an publisher with given id or returns the error. +func (m *PublisherDAO) FindByID(id string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&publisher) + return publisher, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *PublisherDAO) FindByName(name string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"name": name, "model": PUBLISHERMODEL}).One(&publisher) + return publisher, err +} + +// Insert a publisher into database. +func (m *PublisherDAO) Insert(publisher Publisher) error { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Insert(&publisher) + //util.PrintDebug("PublisherDAO.Insert: %v", publisher) + return err +} + +// Upsert a publisher into database. +func (m *PublisherDAO) Upsert(publisher Publisher) (*mgo.ChangeInfo, error) { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + info, err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Upsert(bson.M{"name": publisher.Name}, &publisher) + return info, err +} + +// Delete an existing publisher. +func (m *PublisherDAO) Delete(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Remove(&publisher) + return err +} + +// Update an existing publisher. +func (m *PublisherDAO) Update(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).UpdateId(publisher.ID, &publisher) + return err +} diff --git a/go/pkg/comics/publisher_test.go b/go/pkg/comics/publisher_test.go new file mode 100644 index 0000000..289c2cf --- /dev/null +++ b/go/pkg/comics/publisher_test.go @@ -0,0 +1,87 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var publisherModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestPublisherModel(t *testing.T) { + m := Publisher{} + if reflect.TypeOf(m).NumField() != len(publisherModelTestTable) { + t.Fail() + } + for index, testData := range publisherModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPublishers(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} + +func TestInsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + publishers []Publisher + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "CrossGen" + publisherDao.Insert(publisher) + publishers, _ = publisherDao.FindAll() + if len(publishers) != 1 { + t.Fail() + } +} + +func TestUpsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Marvel" + publisherDao.Upsert(publisher) + publishers, _ := publisherDao.FindAll() + if len(publishers) != 2 { + t.Fail() + } +} + +func TestDeletePublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, _ := publisherDao.FindAll() + for _, publisher := range publishers { + publisherDao.Delete(publisher) + } + publishers, _ = publisherDao.FindAll() + if len(publishers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/comics/publisher_views.go b/go/pkg/comics/publisher_views.go new file mode 100644 index 0000000..e6acfc9 --- /dev/null +++ b/go/pkg/comics/publisher_views.go @@ -0,0 +1,70 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // ComicsPublisherTemplate defines name of template file for comics publishers + ComicsPublisherTemplate = "comics/publishers.html" + // ComicsPublisherDetailsTemplate defines name of template file for comics publishers + ComicsPublisherDetailsTemplate = "comics/publisher.html" +) + +func showPublisherList(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + if publishers, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comics Publisher List", "payload": publishers}, ComicsPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherDetails(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + publisherid := c.Param("publisher_id") + if publisher, err := dao.FindByID(publisherid); err == nil { + util.Render(c, gin.H{"title": "Comics Publisher", "payload": publisher, "action": util.SaveAction}, ComicsPublisherDetailsTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherCreation(c *gin.Context) { + var publisher = Publisher{} + util.Render(c, gin.H{"title": "Comics Publisher Creation", "payload": publisher, "action": util.AddAction}, ComicsPublisherTemplate) +} + +func validatePublisherDetails(c *gin.Context) { + name := c.PostForm("name") + action := c.PostForm("action") + publisherid := c.PostForm("publisherid") + + var err error + var dao = PublisherDAO{Db: dao.KontorDb} + var publisher = Publisher{} + + switch action { + case util.AddAction: + publisher.Name = name + _, err = dao.Upsert(publisher) + case util.SaveAction: + publisher, _ = dao.FindByID(publisherid) + publisher.Name = name + err = dao.Update(publisher) + case util.DeleteAction: + publisher, _ = dao.FindByID(publisherid) + err = dao.Delete(publisher) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/comics/publisher") + } else { + c.HTML(http.StatusBadRequest, "comics/publisher.html", gin.H{ + "ErrorTitle": "Publisher Creation Failed", + "ErrorMessage": err.Error()}) + } +} diff --git a/go/pkg/comics/routes.go b/go/pkg/comics/routes.go new file mode 100644 index 0000000..784704e --- /dev/null +++ b/go/pkg/comics/routes.go @@ -0,0 +1,28 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for comic related data. +func GetRoutes(router *gin.Engine) { + comicRoutes := router.Group("/comics") + { + comicRoutes.GET("/", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/artist", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/artist/view/:artist_id", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/artist/create", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.POST("/artist/create", auth.EnsureLoggedIn(), showArtistList) + comicRoutes.GET("/comic", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/comic/view/:comic_id", auth.EnsureLoggedIn(), showComic) + comicRoutes.GET("/comic/create", auth.EnsureLoggedIn(), showComicList) + comicRoutes.POST("/comic/create", auth.EnsureLoggedIn(), showComicList) + comicRoutes.GET("/publisher", auth.EnsureLoggedIn(), showPublisherList) + comicRoutes.POST("/publisher", auth.EnsureLoggedIn(), showPublisherList) + comicRoutes.GET("/publisher/view/:publisher_id", auth.EnsureLoggedIn(), showPublisherDetails) + comicRoutes.POST("/publisher/validate", auth.EnsureLoggedIn(), validatePublisherDetails) + comicRoutes.GET("/publisher/create", auth.EnsureLoggedIn(), showPublisherCreation) + } +} diff --git a/go/pkg/comics/views.go b/go/pkg/comics/views.go new file mode 100644 index 0000000..f1e9592 --- /dev/null +++ b/go/pkg/comics/views.go @@ -0,0 +1,28 @@ +package comics + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +func showComicList(c *gin.Context) { + var dao = ComicDAO{Db: dao.KontorDb} + if comics, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Comics", "payload": comics}, "comics.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showComic(c *gin.Context) { + var dao = ComicDAO{Db: dao.KontorDb} + comicID := c.Param("comic_id") + if comic, err := dao.FindByID(comicID); err == nil { + util.Render(c, gin.H{"title": "Comic Details", "payload": comic, "action": util.SaveAction}, "comic.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/context/comic/responses/comic.go b/go/pkg/context/comic/responses/comic.go new file mode 100644 index 0000000..a8b6c06 --- /dev/null +++ b/go/pkg/context/comic/responses/comic.go @@ -0,0 +1,15 @@ +package responses + +// Response is the Ping Response +type Response struct { + Message string `json:"message"` +} + +type ComicList struct { + Comics []Comic `json:"comics"` +} + +type Comic struct { + ID string `json:"id"` + Title string `json:"title"` +} diff --git a/go/pkg/context/comic/routing/endpoints.go b/go/pkg/context/comic/routing/endpoints.go new file mode 100644 index 0000000..abe4187 --- /dev/null +++ b/go/pkg/context/comic/routing/endpoints.go @@ -0,0 +1,33 @@ +package routing + +import ( + "net/http" + + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/context/comic/responses" + "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/app" + responseFactory "gitlab.com/tpeetz-kontor/kontor-go/cmd/pkg/infrastructure/response" +) + +// Handler is the http.Handler for this request +type Handler struct { + app *app.Application +} + +// NewHandler will create a new Handler to handle this request +func NewHandler(app *app.Application) *Handler { + return &Handler{app} +} + +// Handle will handle the incoming request +func (handler *Handler) ComicList(response http.ResponseWriter, request *http.Request) { + handler.app.Logger.Info("Ping Handler Dispatched.") + + responseFactory.Send( + response, + http.StatusOK, + &responses.ComicList{ + Comics: []responses.Comic{{ID: "123", Title: "Comic1"}, {ID: "123", Title: "Comic1"}}, + }, + handler.app.Config.HTTP.Content, + ) +} diff --git a/go/pkg/dao/database.go b/go/pkg/dao/database.go new file mode 100644 index 0000000..1a0f4c1 --- /dev/null +++ b/go/pkg/dao/database.go @@ -0,0 +1,31 @@ +package dao + +import ( + "log" + + mgo "gopkg.in/mgo.v2" +) + +// BaseDAO definess the connection parameters to a MongoDB instance. +type BaseDAO struct { + Server string + Database string + MongoDb *mgo.Database +} + +var ( + // KontorDb has the database connection to the productive MongoDB instance. + KontorDb = BaseDAO{Server: "localhost", Database: "kontor"} + // TestDb has the database connection to the test MongoDB instance. + TestDb = BaseDAO{Server: "localhost", Database: "kontor_test"} +) + +// Connect instantiates the database session. +func (m *BaseDAO) Connect() { + session, err := mgo.Dial(m.Server) + if err != nil { + //util.PrintDebug("Connect: %v", err) + log.Fatal(err) + } + m.MongoDb = session.DB(m.Database) +} diff --git a/go/pkg/dao/database_test.go b/go/pkg/dao/database_test.go new file mode 100644 index 0000000..52a204c --- /dev/null +++ b/go/pkg/dao/database_test.go @@ -0,0 +1,51 @@ +package dao + +import ( + "reflect" + "testing" +) + + +var baseDaoTestTable = []struct { + name string + typeName string +}{ + {"Server", "string"}, + {"Database", "string"}, + {"MongoDb", "ptr"}, +} + +func TestCheckBaseDao(t *testing.T) { + d := BaseDAO{} + for index, testData := range baseDaoTestTable { + givenType := reflect.TypeOf(d).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestConnectDb(t *testing.T) { + d := BaseDAO{} + d.Connect() + if d.MongoDb == nil { + t.Fail() + } +} + +func TestDatabasesConfig(t *testing.T) { + kontorDb := KontorDb + if kontorDb.Server != "localhost" { + t.Fail() + } + if kontorDb.Database != "kontor" { + t.Fail() + } + testDb := TestDb + if testDb.Server != "localhost" { + t.Fail() + } + if testDb.Database != "kontor_test" { + t.Fail() + } +} diff --git a/go/pkg/infrastructure/app/factory.go b/go/pkg/infrastructure/app/factory.go new file mode 100644 index 0000000..3bc8ce7 --- /dev/null +++ b/go/pkg/infrastructure/app/factory.go @@ -0,0 +1,50 @@ +package app + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gorilla/mux" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/config" + "go.uber.org/zap" +) + +// Application is our general purpose Application struct +type Application struct { + Server *http.Server + Router *mux.Router + Logger *zap.Logger + Config *config.Config +} + +// Run will run the Application server +func (app *Application) Run() { + app.Logger.Info("App started...") + err := app.Server.ListenAndServe() + + if err != nil { + fmt.Println(err) + } +} + +// WaitForShutdown is a graceful way to handle server shutdown events +func WaitForShutdown(application *Application) { + // Create a channel to listen for OS signals + interruptChan := make(chan os.Signal, 1) + signal.Notify(interruptChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + // Block until we receive a signal to our channel + <-interruptChan + + application.Logger.Info("Received shutdown signal, gracefully terminating") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + application.Server.Shutdown(ctx) + os.Exit(0) +} diff --git a/go/pkg/infrastructure/config/factory.go b/go/pkg/infrastructure/config/factory.go new file mode 100644 index 0000000..ec411f8 --- /dev/null +++ b/go/pkg/infrastructure/config/factory.go @@ -0,0 +1,48 @@ +package config + +import "os" + +// AppConfig is the Application configuration struct +type AppConfig struct { + Name string + Version string + Token string +} + +// HTTPConfig is the Application HTTP configuration +type HTTPConfig struct { + Content string + Problem string + Port string +} + +// Config is the Configuration struct +type Config struct { + App AppConfig + HTTP HTTPConfig +} + +// New returns a new Config Struct +func New() *Config { + return &Config{ + App: AppConfig{ + Name: env("APP_NAME", "Kontor"), + Version: env("APP_VERSION", "v1.0"), + Token: env("APP_TOKEN", ""), + }, + HTTP: HTTPConfig{ + Content: env("HTTP_CONTENT_TYPE", "application/json"), + Problem: env("HTTP_PROBLEM", "application/problem+json"), + Port: env("HTTP_PORT", ":8086"), + }, + } +} + +// env is a simple helper function to read an environment variable or return a default value +func env(key string, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + + return defaultValue +} diff --git a/go/pkg/infrastructure/kernel/app.go b/go/pkg/infrastructure/kernel/app.go new file mode 100644 index 0000000..39eb5b7 --- /dev/null +++ b/go/pkg/infrastructure/kernel/app.go @@ -0,0 +1,91 @@ +package kernel + +import ( + "context" + "net/http" + "time" + + "github.com/google/uuid" + gohandlers "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/app" + "gitlab.com/tpeetz-kontor/kontor-go/pkg/infrastructure/config" + "go.uber.org/zap" +) + +// Boot the Application +func Boot() *app.Application { + // Configuration + config := bootConfig() + + // Router + router := bootRouter() + + // CORS + corsHandler := gohandlers.CORS(gohandlers.AllowedOrigins([]string{"*"})) + + // Logger + logger := bootLogger() + defer logger.Sync() // flushes buffer, if any + + // Create and return and Application + return &app.Application{ + Server: &http.Server{ + Addr: config.HTTP.Port, + Handler: corsHandler(requestIDMiddleware(router)), + IdleTimeout: 120 * time.Second, + ReadTimeout: 1 * time.Second, + WriteTimeout: 1 * time.Second, + }, + Router: router, + Logger: logger, + Config: config, + } +} + +func bootConfig() *config.Config { + return config.New() +} + +func bootRouter() *mux.Router { + return mux.NewRouter() +} + +func bootLogger() *zap.Logger { + logger, logError := zap.NewProduction() + if logError != nil { + panic(logError) + } + + return logger +} + +// ContextKey is used for context.Context value. The value requires a key that is not primitive type. +type ContextKey string + +// ContextKeyRequestID is the ContextKey for RequestID +const ContextKeyRequestID ContextKey = "requestID" + +func requestIDMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + id := uuid.New() + + ctx = context.WithValue(ctx, ContextKeyRequestID, id.String()) + + r = r.WithContext(ctx) + + bootLogger().Debug("Incoming request", + zap.String("method", r.Method), + zap.String("uri", r.RequestURI), + zap.String("addr", r.RemoteAddr), zap.String("id", id.String()), + ) + + next.ServeHTTP(w, r) + + bootLogger().Debug("Finished handling http req. %s", + zap.String("id", id.String())) + }) +} diff --git a/go/pkg/infrastructure/response/response.go b/go/pkg/infrastructure/response/response.go new file mode 100644 index 0000000..f8def4a --- /dev/null +++ b/go/pkg/infrastructure/response/response.go @@ -0,0 +1,20 @@ +package response + +import ( + "encoding/json" + "net/http" +) + +// Response is a generic HTTP Response Struct +type Response struct { + Data string `json:"data"` +} + +// Send a HTTP Response +func Send(responseWriter http.ResponseWriter, code int, payload interface{}, contentType string) { + response, _ := json.Marshal(payload) + + responseWriter.Header().Set("Content-Type", contentType) + responseWriter.WriteHeader(code) + responseWriter.Write(response) +} diff --git a/go/pkg/library/author.go b/go/pkg/library/author.go new file mode 100644 index 0000000..2aafcd7 --- /dev/null +++ b/go/pkg/library/author.go @@ -0,0 +1,12 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Author defines the data model for library authors with id and name. +type Author struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/library/author_dao.go b/go/pkg/library/author_dao.go new file mode 100644 index 0000000..aa5f189 --- /dev/null +++ b/go/pkg/library/author_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// AuthorDAO extends the type BaseDAO. +type AuthorDAO struct { + Db dao.BaseDAO +} + +const ( + // AUTHORCOLLECTION defines the collection name for storing authors. + AUTHORCOLLECTION = "author" + // AUTHORMODEL defines the name of the author data model. + AUTHORMODEL = "kontor.library.author" +) + +// FindAll retrieves the list of authors from the database. +func (m *AuthorDAO) FindAll() ([]Author, error) { + m.Db.Connect() + var authors []Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Find(bson.M{"model": AUTHORMODEL}).All(&authors) + return authors, err +} + +// FindByID returns an author with given id or returns the error. +func (m *AuthorDAO) FindByID(id string) (Author, error) { + m.Db.Connect() + var author Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&author) + return author, err +} + +// FindByName returns an author with given name or returns the error. +func (m *AuthorDAO) FindByName(name string) (Author, error) { + m.Db.Connect() + var author Author + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Find(bson.M{"name": name, "model": AUTHORMODEL}).One(&author) + return author, err +} + +// Insert a author into database. +func (m *AuthorDAO) Insert(author Author) error { + m.Db.Connect() + author.Model = AUTHORMODEL + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Insert(&author) + return err +} + +// Upsert a author into database. +func (m *AuthorDAO) Upsert(author Author) (*mgo.ChangeInfo, error) { + m.Db.Connect() + author.Model = AUTHORMODEL + info, err := m.Db.MongoDb.C(AUTHORCOLLECTION).Upsert(bson.M{"name": author.Name}, &author) + return info, err +} + +// Delete an existing author. +func (m *AuthorDAO) Delete(author Author) error { + m.Db.Connect() + err := m.Db.MongoDb.C(AUTHORCOLLECTION).Remove(&author) + return err +} + +// Update an existing author. +func (m *AuthorDAO) Update(author Author) error { + m.Db.Connect() + err := m.Db.MongoDb.C(AUTHORCOLLECTION).UpdateId(author.ID, &author) + return err +} diff --git a/go/pkg/library/author_test.go b/go/pkg/library/author_test.go new file mode 100644 index 0000000..54a6537 --- /dev/null +++ b/go/pkg/library/author_test.go @@ -0,0 +1,99 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var authorModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestAuthorModel(t *testing.T) { + m := Author{} + if reflect.TypeOf(m).NumField() != len(authorModelTestTable) { + t.Fail() + } + for index, testData := range authorModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListAuthors(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 0 { + t.Fail() + } +} + +func TestInsertAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + author = Author{} + authors []Author + ) + author.ID = bson.NewObjectId() + author.Name = "Packt Publishing" + err := authorDao.Insert(author) + if err != nil { + t.Fail() + } + authors, err = authorDao.FindAll() + if len(authors) != 1 { + t.Fail() + } +} + +func TestUpsertAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + var author = Author{} + author.ID = bson.NewObjectId() + author.Name = "Hansa Verlag" + authorDao.Upsert(author) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 2 { + t.Fail() + } +} + +func TestDeleteAuthor(t *testing.T) { + var ( + authorDao = AuthorDAO{Db: dao.TestDb} + ) + authors, err := authorDao.FindAll() + if err != nil { + t.Fail() + } + for _, author := range authors { + authorDao.Delete(author) + } + authors, err = authorDao.FindAll() + if err != nil { + t.Fail() + } + if len(authors) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/book.go b/go/pkg/library/book.go new file mode 100644 index 0000000..855f71c --- /dev/null +++ b/go/pkg/library/book.go @@ -0,0 +1,18 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Book defines the data model for library books with id, title, author, publisher, +// isbn, year and edition. +type Book struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + Author string `json:"author" bson:"author"` + Publisher bson.ObjectId `json:"publisher" bson:"publisher,omitempty"` + Isbn string `json:"isbn" bson:"isbn,omitempty"` + Year int `json:"year" bson:"year,omitempty"` + Edition string `json:"edition" bson:"edition,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/library/book_dao.go b/go/pkg/library/book_dao.go new file mode 100644 index 0000000..ddfea6a --- /dev/null +++ b/go/pkg/library/book_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// BookDAO extends the type BaseDAO. +type BookDAO struct { + Db dao.BaseDAO +} + +const ( + // BOOKCOLLECTION defines the collection name for storing books. + BOOKCOLLECTION = "book" + // BOOKMODEL defines the name of the book data model. + BOOKMODEL = "kontor.library.book" +) + +// FindAll retrieves the list of books from the database. +func (m *BookDAO) FindAll() ([]Book, error) { + m.Db.Connect() + var books []Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).Find(bson.M{"model": BOOKMODEL}).All(&books) + return books, err +} + +// FindByID returns an book with given id or returns the error. +func (m *BookDAO) FindByID(id string) (Book, error) { + m.Db.Connect() + var book Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&book) + return book, err +} + +// FindByTitle returns a book with given title or returns the error. +func (m *BookDAO) FindByTitle(title string) (Book, error) { + m.Db.Connect() + var book Book + err := m.Db.MongoDb.C(BOOKCOLLECTION).Find(bson.M{"title": title, "model": BOOKMODEL}).One(&book) + return book, err +} + +// Insert a book into database. +func (m *BookDAO) Insert(book Book) error { + m.Db.Connect() + book.Model = BOOKMODEL + err := m.Db.MongoDb.C(BOOKCOLLECTION).Insert(&book) + return err +} + +// Upsert a book into database. +func (m *BookDAO) Upsert(book Book) (*mgo.ChangeInfo, error) { + m.Db.Connect() + book.Model = BOOKMODEL + info, err := m.Db.MongoDb.C(BOOKCOLLECTION).Upsert(bson.M{"title": book.Title}, &book) + return info, err +} + +// Delete an existing book. +func (m *BookDAO) Delete(book Book) error { + m.Db.Connect() + err := m.Db.MongoDb.C(BOOKCOLLECTION).Remove(&book) + return err +} + +// Update an existing book. +func (m *BookDAO) Update(book Book) error { + m.Db.Connect() + err := m.Db.MongoDb.C(BOOKCOLLECTION).UpdateId(book.ID, &book) + return err +} diff --git a/go/pkg/library/book_test.go b/go/pkg/library/book_test.go new file mode 100644 index 0000000..7a4f1f1 --- /dev/null +++ b/go/pkg/library/book_test.go @@ -0,0 +1,107 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var bookModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Title", "string"}, + {"Author", "string"}, + {"Publisher", "string"}, + {"Isbn", "string"}, + {"Year", "int"}, + {"Edition", "string"}, + {"Model", "string"}, +} + +func TestBookModel(t *testing.T) { + m := Book{} + if reflect.TypeOf(m).NumField() != len(bookModelTestTable) { + t.Fail() + } + for index, testData := range bookModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListBooks(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + ) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 0 { + t.Fail() + } +} + +func TestInsertBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + book = Book{} + books []Book + ) + book.ID = bson.NewObjectId() + book.Title = "Packt Publishing" + err := bookDao.Insert(book) + if err != nil { + t.Fail() + } + books, err = bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 1 { + t.Fail() + } +} + +func TestUpsertBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + book = Book{} + ) + book.ID = bson.NewObjectId() + book.Title = "Hansa Verlag" + bookDao.Upsert(book) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 2 { + t.Fail() + } +} + +func TestDeleteBook(t *testing.T) { + var ( + bookDao = BookDAO{Db: dao.TestDb} + ) + books, err := bookDao.FindAll() + if err != nil { + t.Fail() + } + for _, book := range books { + bookDao.Delete(book) + } + books, err = bookDao.FindAll() + if err != nil { + t.Fail() + } + if len(books) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/publisher.go b/go/pkg/library/publisher.go new file mode 100644 index 0000000..b7eb84d --- /dev/null +++ b/go/pkg/library/publisher.go @@ -0,0 +1,12 @@ +package library + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Publisher defines the data model for library publishers with id and name. +type Publisher struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model"` +} diff --git a/go/pkg/library/publisher_dao.go b/go/pkg/library/publisher_dao.go new file mode 100644 index 0000000..0e48cf5 --- /dev/null +++ b/go/pkg/library/publisher_dao.go @@ -0,0 +1,74 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PublisherDAO extends the type BaseDAO. +type PublisherDAO struct { + Db dao.BaseDAO +} + +const ( + // PUBLISHERCOLLECTION defines the collection name for storing publishers. + PUBLISHERCOLLECTION = "publisher" + // PUBLISHERMODEL defines the name of the publisher data model. + PUBLISHERMODEL = "kontor.library.publisher" +) + +// FindAll retrieves the list of publishers from the database. +func (m *PublisherDAO) FindAll() ([]Publisher, error) { + m.Db.Connect() + var publishers []Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"model": PUBLISHERMODEL}).All(&publishers) + return publishers, err +} + +// FindByID returns an publisher with given id or returns the error. +func (m *PublisherDAO) FindByID(id string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&publisher) + return publisher, err +} + +// FindByName returns an comic with given name or returns the error. +func (m *PublisherDAO) FindByName(name string) (Publisher, error) { + m.Db.Connect() + var publisher Publisher + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Find(bson.M{"name": name, "model": PUBLISHERMODEL}).One(&publisher) + return publisher, err +} + +// Insert a publisher into database. +func (m *PublisherDAO) Insert(publisher Publisher) error { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Insert(&publisher) + return err +} + +// Upsert a publisher into database. +func (m *PublisherDAO) Upsert(publisher Publisher) (*mgo.ChangeInfo, error) { + m.Db.Connect() + publisher.Model = PUBLISHERMODEL + info, err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Upsert(bson.M{"name": publisher.Name}, &publisher) + return info, err +} + +// Delete an existing publisher. +func (m *PublisherDAO) Delete(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).Remove(&publisher) + return err +} + +// Update an existing publisher. +func (m *PublisherDAO) Update(publisher Publisher) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PUBLISHERCOLLECTION).UpdateId(publisher.ID, &publisher) + return err +} diff --git a/go/pkg/library/publisher_test.go b/go/pkg/library/publisher_test.go new file mode 100644 index 0000000..1c680f3 --- /dev/null +++ b/go/pkg/library/publisher_test.go @@ -0,0 +1,103 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var publisherModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestPublisherModel(t *testing.T) { + m := Publisher{} + if reflect.TypeOf(m).NumField() != len(publisherModelTestTable) { + t.Fail() + } + for index, testData := range publisherModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + + } +} + +func TestListPublishers(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} + +func TestInsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + publishers []Publisher + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Packt Publishing" + err := publisherDao.Insert(publisher) + if err != nil { + t.Fail() + } + publishers, err = publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 1 { + t.Fail() + } +} + +func TestUpsertPublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + publisher = Publisher{} + ) + publisher.ID = bson.NewObjectId() + publisher.Name = "Hansa Verlag" + publisherDao.Upsert(publisher) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 2 { + t.Fail() + } +} + +func TestDeletePublisher(t *testing.T) { + var ( + publisherDao = PublisherDAO{Db: dao.TestDb} + ) + publishers, err := publisherDao.FindAll() + if err != nil { + t.Fail() + } + for _, publisher := range publishers { + publisherDao.Delete(publisher) + } + publishers, err = publisherDao.FindAll() + if err != nil { + t.Fail() + } + if len(publishers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/library/publisher_views.go b/go/pkg/library/publisher_views.go new file mode 100644 index 0000000..233169a --- /dev/null +++ b/go/pkg/library/publisher_views.go @@ -0,0 +1,68 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // LibraryPublisherTemplate defines name of template file for comics publishers + LibraryPublisherTemplate = "library/publishers.html" +) + +func showPublisherList(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + if publishers, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Library Publisher List", "payload": publishers}, LibraryPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherDetails(c *gin.Context) { + var dao = PublisherDAO{Db: dao.KontorDb} + publisherid := c.Param("publisher_id") + if publisher, err := dao.FindByID(publisherid); err == nil { + util.Render(c, gin.H{"title": "Library Publisher", "payload": publisher, "action": util.SaveAction}, LibraryPublisherTemplate) + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} + +func showPublisherCreation(c *gin.Context) { + var publisher = Publisher{} + util.Render(c, gin.H{"title": "Library Publisher Creation", "payload": publisher, "action": util.AddAction}, LibraryPublisherTemplate) +} + +func validatePublisherDetails(c *gin.Context) { + name := c.PostForm("name") + action := c.PostForm("action") + publisherid := c.PostForm("publisherid") + + var err error + var dao = PublisherDAO{Db: dao.KontorDb} + var publisher = Publisher{} + + switch action { + case util.AddAction: + publisher.Name = name + _, err = dao.Upsert(publisher) + case util.SaveAction: + publisher, _ = dao.FindByID(publisherid) + publisher.Name = name + err = dao.Update(publisher) + case util.DeleteAction: + publisher, _ = dao.FindByID(publisherid) + err = dao.Delete(publisher) + } + if err == nil { + c.Redirect(http.StatusTemporaryRedirect, "/library/publisher") + } else { + c.HTML(http.StatusBadRequest, "library/publisher.html", gin.H{ + "ErrorTitle": "Publisher Creation Failed", + "ErrorMessage": err.Error()}) + } +} diff --git a/go/pkg/library/routes.go b/go/pkg/library/routes.go new file mode 100644 index 0000000..ea93fd5 --- /dev/null +++ b/go/pkg/library/routes.go @@ -0,0 +1,22 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for library related data. +func GetRoutes(router *gin.Engine) { + libraryRoutes := router.Group("/library") + { + libraryRoutes.GET("/", auth.EnsureLoggedIn(), showBookList) + libraryRoutes.GET("/book", auth.EnsureLoggedIn(), showBookList) + libraryRoutes.GET("/author", auth.EnsureLoggedIn(), showAuthorList) + libraryRoutes.GET("/publisher", auth.EnsureLoggedIn(), showPublisherList) + libraryRoutes.POST("/publisher", auth.EnsureLoggedIn(), showPublisherList) + libraryRoutes.GET("/publisher/view/:publisher_id", auth.EnsureLoggedIn(), showPublisherDetails) + libraryRoutes.POST("/publisher/validate", auth.EnsureLoggedIn(), validatePublisherDetails) + libraryRoutes.GET("/publisher/create", auth.EnsureLoggedIn(), showPublisherCreation) + } +} diff --git a/go/pkg/library/views.go b/go/pkg/library/views.go new file mode 100644 index 0000000..4b4bcc6 --- /dev/null +++ b/go/pkg/library/views.go @@ -0,0 +1,24 @@ +package library + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showAuthorList(c *gin.Context) { + var authorDao = AuthorDAO{Db: dao.KontorDb} + if authors, err := authorDao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Author List", "payload": authors}, "library/authors.html") + } +} + +func showBookList(c *gin.Context) { + var bookDao = BookDAO{Db: dao.KontorDb} + if books, err := bookDao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "Book List", "payload": books}, "library/books.html") + } else { + util.Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/index.html") + } +} diff --git a/go/pkg/office/routes.go b/go/pkg/office/routes.go new file mode 100644 index 0000000..8674071 --- /dev/null +++ b/go/pkg/office/routes.go @@ -0,0 +1,13 @@ +package office + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for office related data. +func GetRoutes(router *gin.Engine) { + officeRoutes := router.Group("/office") + officeRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/office/views.go b/go/pkg/office/views.go new file mode 100644 index 0000000..2fd1504 --- /dev/null +++ b/go/pkg/office/views.go @@ -0,0 +1,11 @@ +package office + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "Home Office", "payload": nil}, "office/index.html") +} diff --git a/go/pkg/properties/root.go b/go/pkg/properties/root.go new file mode 100644 index 0000000..c80e136 --- /dev/null +++ b/go/pkg/properties/root.go @@ -0,0 +1,25 @@ +package properties + +var ( + // Version defines the version of the web application kontor. + Version = "undefined" + // Debug defines the property debug to be used for more verbose output. + Debug = false + // Port defines port number under the web application is reachable. + Port = 8500 +) + +// SetVersion sets Version with given value. +func SetVersion(value string) { + Version = value +} + +// SetDebug sets Debug with given value. +func SetDebug(value bool) { + Debug = value +} + +// SetPort sets Port with given value. +func SetPort(value int) { + Port = value +} diff --git a/go/pkg/setup/data.go b/go/pkg/setup/data.go new file mode 100644 index 0000000..e45e813 --- /dev/null +++ b/go/pkg/setup/data.go @@ -0,0 +1,20 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tysc" + "log" +) + +// CheckTradeYourSportsCardsData checks if the TYSC releated data is available. +func CheckTradeYourSportsCardsData() { + log.Printf("Check data for TradeYourSportsCards module") + var sport = tysc.SportDAO{Db: dao.KontorDb} + sport.Upsert(tysc.Sport{Name: "Football"}) + football, _ := sport.FindByName("Football") + sport.Upsert(tysc.Sport{Name: "Baseball"}) + sport.Upsert(tysc.Sport{Name: "Basketball"}) + sport.Upsert(tysc.Sport{Name: "Hockey"}) + var position = tysc.PositionDAO{Db: dao.KontorDb} + position.Upsert(tysc.Position{Name: "WR", Description: "Wide Receiver", Sport: football.ID}) +} diff --git a/go/pkg/setup/routes.go b/go/pkg/setup/routes.go new file mode 100644 index 0000000..3b1a9b6 --- /dev/null +++ b/go/pkg/setup/routes.go @@ -0,0 +1,38 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/admin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/comics" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/library" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/office" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tradingcards" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/tysc" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +// InitializeRoutes setup the routes for Kontor web application. +func InitializeRoutes(router *gin.Engine) { + + // Use the setUserStatus middleware for every route to set a flag + // indicating whether the request was from an authenticated user or not + router.Use(auth.SetSessionStatus()) + + // Handle the index route + router.GET("/", util.ShowIndexPage) + + userRoutes := router.Group("/user") + { + userRoutes.GET("/login", auth.EnsureNotLoggedIn(), admin.ShowLoginPage) + userRoutes.POST("/login", auth.EnsureNotLoggedIn(), admin.PerformLogin) + userRoutes.GET("/logout", auth.EnsureLoggedIn(), admin.Logout) + } + admin.GetRoutes(router) + comics.GetRoutes(router) + library.GetRoutes(router) + office.GetRoutes(router) + tradingcards.GetRoutes(router) + tysc.GetRoutes(router) +} diff --git a/go/pkg/setup/session.go b/go/pkg/setup/session.go new file mode 100644 index 0000000..5fe184a --- /dev/null +++ b/go/pkg/setup/session.go @@ -0,0 +1,15 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" +) + +// CleanupSessions removes all sessions from database. +func CleanupSessions() { + sessionDao := auth.SessionDAO{Db: dao.KontorDb} + sessions, _ := sessionDao.FindAll() + for _, session := range sessions { + sessionDao.Delete(session) + } +} diff --git a/go/pkg/setup/user.go b/go/pkg/setup/user.go new file mode 100644 index 0000000..5272ed3 --- /dev/null +++ b/go/pkg/setup/user.go @@ -0,0 +1,20 @@ +package setup + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/admin" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + "gopkg.in/mgo.v2/bson" +) + +// CheckUserList ensures that at least the admin user is available. +func CheckUserList() { + var userDao = admin.UserDAO{Db: dao.KontorDb} + users, err := userDao.FindAll() + if err == nil && len(users) == 0 { + password, _ := admin.HashPassword("admin") + id := bson.NewObjectId() + user := admin.User{ID: id, Username: "admin", Password: password, Firstname: "Administrator", IsAdmin: true} + userDao.Insert(user) + } +} diff --git a/go/pkg/tradingcards/routes.go b/go/pkg/tradingcards/routes.go new file mode 100644 index 0000000..d5d3aad --- /dev/null +++ b/go/pkg/tradingcards/routes.go @@ -0,0 +1,13 @@ +package tradingcards + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for tradingcards related data. +func GetRoutes(router *gin.Engine) { + tradingCardsRoutes := router.Group("/tradingcards") + tradingCardsRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/tradingcards/views.go b/go/pkg/tradingcards/views.go new file mode 100644 index 0000000..1d4f104 --- /dev/null +++ b/go/pkg/tradingcards/views.go @@ -0,0 +1,11 @@ +package tradingcards + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "Trading Cards", "payload": nil}, "tradingcards/index.html") +} diff --git a/go/pkg/tysc/card.go b/go/pkg/tysc/card.go new file mode 100644 index 0000000..e2555b5 --- /dev/null +++ b/go/pkg/tysc/card.go @@ -0,0 +1,20 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Card defines the data model for TYSC cards. +type Card struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Player bson.ObjectId `json:"player" bson:"player,omitempty"` + Team bson.ObjectId `json:"team" bson:"team,omitempty"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + CardSet bson.ObjectId `json:"cardset" bson:"cardset,omitempty"` + ParallelSet bson.ObjectId `json:"parallelset" bson:"paralelset,omitempty"` + InsertSet bson.ObjectId `json:"insertset" bson:"insertset,omitempty"` + Rookie bool `json:"rookie" bson:"rookie,omitempty"` + Year int `json:"year" bson:"year,omitempty"` + Number int `json:"number" bson:"number,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/card_dao.go b/go/pkg/tysc/card_dao.go new file mode 100644 index 0000000..8fa4d4b --- /dev/null +++ b/go/pkg/tysc/card_dao.go @@ -0,0 +1,66 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// CardDAO extends the type BaseDAO. +type CardDAO struct { + Db dao.BaseDAO +} + +const ( + // CARDCOLLECTION defines the collection name for storing cards. + CARDCOLLECTION = "card" + // CARDMODEL defines the name of the card data model. + CARDMODEL = "kontor.tysc.card" +) + +// FindAll retrieves the list of cards from the database. +func (m *CardDAO) FindAll() ([]Card, error) { + m.Db.Connect() + var cards []Card + err := m.Db.MongoDb.C(CARDCOLLECTION).Find(bson.M{"model": CARDMODEL}).All(&cards) + return cards, err +} + +// FindByID returns an card with given id or returns the error. +func (m *CardDAO) FindByID(id string) (Card, error) { + m.Db.Connect() + var card Card + err := m.Db.MongoDb.C(CARDCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&card) + return card, err +} + +// Insert a card into database. +func (m *CardDAO) Insert(card Card) error { + m.Db.Connect() + card.Model = CARDMODEL + err := m.Db.MongoDb.C(CARDCOLLECTION).Insert(&card) + return err +} + +// Upsert a card into database. +func (m *CardDAO) Upsert(card Card) (*mgo.ChangeInfo, error) { + m.Db.Connect() + card.Model = CARDMODEL + info, err := m.Db.MongoDb.C(CARDCOLLECTION).Upsert(bson.M{"number": card.Number}, &card) + return info, err +} + +// Update an existing card. +func (m *CardDAO) Update(card Card) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDCOLLECTION).UpdateId(card.ID, &card) + return err +} + +// Delete an existing card. +func (m *CardDAO) Delete(card Card) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDCOLLECTION).Remove(&card) + return err +} diff --git a/go/pkg/tysc/card_test.go b/go/pkg/tysc/card_test.go new file mode 100644 index 0000000..e5c0afb --- /dev/null +++ b/go/pkg/tysc/card_test.go @@ -0,0 +1,110 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var cardModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Player", "string"}, + {"Team", "string"}, + {"Manufacturer", "string"}, + {"CardSet", "string"}, + {"ParallelSet", "string"}, + {"InsertSet", "string"}, + {"Rookie", "bool"}, + {"Year", "int"}, + {"Number", "int"}, + {"Model", "string"}, +} + +func TestCardModel(t *testing.T) { + m := Card{} + if reflect.TypeOf(m).NumField() != len(cardModelTestTable) { + t.Fail() + } + for index, testData := range cardModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListCards(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + ) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 0 { + t.Fail() + } +} + +func TestInsertCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + card = Card{} + cards []Card + ) + card.ID = bson.NewObjectId() + card.Number = 1 + err := cardDao.Insert(card) + if err != nil { + t.Fail() + } + cards, err = cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 1 { + t.Fail() + } +} + +func TestUpsertCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + card = Card{} + ) + card.ID = bson.NewObjectId() + card.Number = 2 + cardDao.Upsert(card) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 2 { + t.Fail() + } +} + +func TestDeleteCard(t *testing.T) { + var ( + cardDao = CardDAO{Db: dao.TestDb} + ) + cards, err := cardDao.FindAll() + if err != nil { + t.Fail() + } + for _, card := range cards { + cardDao.Delete(card) + } + cards, err = cardDao.FindAll() + if err != nil { + t.Fail() + } + if len(cards) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/cardset.go b/go/pkg/tysc/cardset.go new file mode 100644 index 0000000..6184a11 --- /dev/null +++ b/go/pkg/tysc/cardset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// CardSet defines the data model for TYSC card sets with id, name and manufacturer. +type CardSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/cardset_dao.go b/go/pkg/tysc/cardset_dao.go new file mode 100644 index 0000000..4158860 --- /dev/null +++ b/go/pkg/tysc/cardset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// CardSetDAO extends the type BaseDAO. +type CardSetDAO struct { + Db dao.BaseDAO +} + +const ( + // CARDSETCOLLECTION defines the collection name for storing cards sets. + CARDSETCOLLECTION = "cardSet" + // CARDSETMODEL defines the name of the card set data model. + CARDSETMODEL = "kontor.tysc.cardSet" +) + +// FindAll retrieves the list of card sets from the database. +func (m *CardSetDAO) FindAll() ([]CardSet, error) { + m.Db.Connect() + var cardSets []CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"model": CARDSETMODEL}).All(&cardSets) + return cardSets, err +} + +// FindByID returns a card set with given id or returns the error. +func (m *CardSetDAO) FindByID(id string) (CardSet, error) { + m.Db.Connect() + var cardSet CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&cardSet) + return cardSet, err +} + +// FindByManufacturer returns a card set with given manufacturer or returns the error. +func (m *CardSetDAO) FindByManufacturer(manufacturer string) ([]CardSet, error) { + m.Db.Connect() + var cardSets []CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"model": CARDSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(&cardSets) + return cardSets, err +} + +// FindByName returns a card set with given name or returns the error. +func (m *CardSetDAO) FindByName(name string) (CardSet, error) { + m.Db.Connect() + var cardSet CardSet + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Find(bson.M{"name": name, "model": CARDSETMODEL}).One(&cardSet) + return cardSet, err +} + +// Insert an card set into database. +func (m *CardSetDAO) Insert(cardSet CardSet) error { + m.Db.Connect() + cardSet.Model = CARDSETMODEL + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Insert(&cardSet) + return err +} + +// Upsert an card set into database. +func (m *CardSetDAO) Upsert(cardSet CardSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + cardSet.Model = CARDSETMODEL + info, err := m.Db.MongoDb.C(CARDSETCOLLECTION).Upsert(bson.M{"name": cardSet.Name}, &cardSet) + return info, err +} + +// Update an existing card set. +func (m *CardSetDAO) Update(cardSet CardSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDSETCOLLECTION).UpdateId(cardSet.ID, &cardSet) + return err +} + +// Delete an existing card set. +func (m *CardSetDAO) Delete(cardSet CardSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(CARDSETCOLLECTION).Remove(&cardSet) + return err +} diff --git a/go/pkg/tysc/cardset_test.go b/go/pkg/tysc/cardset_test.go new file mode 100644 index 0000000..82d7ebc --- /dev/null +++ b/go/pkg/tysc/cardset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var cardsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestCardSetModel(t *testing.T) { + m := CardSet{} + if reflect.TypeOf(m).NumField() != len(cardsetModelTestTable) { + t.Fail() + } + for index, testData := range cardsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListCardSets(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + ) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 0 { + t.Fail() + } +} + +func TestInsertCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + cardSet = CardSet{} + cardSets []CardSet + ) + cardSet.ID = bson.NewObjectId() + cardSet.Name = "test" + err := cardsetDao.Insert(cardSet) + if err != nil { + t.Fail() + } + cardSets, err = cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 1 { + t.Fail() + } +} + +func TestUpsertCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + cardSet = CardSet{} + ) + cardSet.ID = bson.NewObjectId() + cardSet.Name = "test2" + cardsetDao.Upsert(cardSet) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 2 { + t.Fail() + } +} + +func TestDeleteCardSet(t *testing.T) { + var ( + cardsetDao = CardSetDAO{Db: dao.TestDb} + ) + cardSets, err := cardsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, cardSet := range cardSets { + cardsetDao.Delete(cardSet) + } + cardSets, err = cardsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(cardSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/insertset.go b/go/pkg/tysc/insertset.go new file mode 100644 index 0000000..8655d35 --- /dev/null +++ b/go/pkg/tysc/insertset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// InsertSet defines the data model for TYSC inserts sets with id, name and manufacturer. +type InsertSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/insertset_dao.go b/go/pkg/tysc/insertset_dao.go new file mode 100644 index 0000000..dc87ddf --- /dev/null +++ b/go/pkg/tysc/insertset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// InsertSetDAO extends the type BaseDAO. +type InsertSetDAO struct { + Db dao.BaseDAO +} + +const ( + // INSERTSETCOLLECTION defines the collection name for storing insert sets. + INSERTSETCOLLECTION = "insertSet" + // INSERTSETMODEL defines the name of the insert set data model. + INSERTSETMODEL = "kontor.tysc.insertSet" +) + +// FindAll retrieves the list of cards from the database. +func (m *InsertSetDAO) FindAll() ([]InsertSet, error) { + m.Db.Connect() + var insertSets []InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"model": INSERTSETMODEL}).All(&insertSets) + return insertSets, err +} + +// FindByID returns an card with given id or returns the error. +func (m *InsertSetDAO) FindByID(id string) (InsertSet, error) { + m.Db.Connect() + var insertSet InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&insertSet) + return insertSet, err +} + +// FindByManufacturer returns a insert set with given manufacturer or returns the error. +func (m *InsertSetDAO) FindByManufacturer(manufacturer string) ([]InsertSet, error) { + m.Db.Connect() + var insertSets []InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"model": INSERTSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(&insertSets) + return insertSets, err +} + +// FindByName returns a insert set with given name or returns the error. +func (m *InsertSetDAO) FindByName(name string) (InsertSet, error) { + m.Db.Connect() + var insertSet InsertSet + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Find(bson.M{"name": name, "model": INSERTSETMODEL}).One(&insertSet) + return insertSet, err +} + +// Insert an insert set into database. +func (m *InsertSetDAO) Insert(insertSet InsertSet) error { + m.Db.Connect() + insertSet.Model = INSERTSETMODEL + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Insert(&insertSet) + return err +} + +// Upsert an insert set into database. +func (m *InsertSetDAO) Upsert(insertSet InsertSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + insertSet.Model = INSERTSETMODEL + info, err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Upsert(bson.M{"name": insertSet.Name}, &insertSet) + return info, err +} + +// Update an existing insert set. +func (m *InsertSetDAO) Update(insertSet InsertSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).UpdateId(insertSet.ID, &insertSet) + return err +} + +// Delete an existing insert set. +func (m *InsertSetDAO) Delete(insertSet InsertSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(INSERTSETCOLLECTION).Remove(&insertSet) + return err +} diff --git a/go/pkg/tysc/insertset_test.go b/go/pkg/tysc/insertset_test.go new file mode 100644 index 0000000..b4d4152 --- /dev/null +++ b/go/pkg/tysc/insertset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var insertsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestInsertSetModel(t *testing.T) { + m := InsertSet{} + if reflect.TypeOf(m).NumField() != len(insertsetModelTestTable) { + t.Fail() + } + for index, testData := range insertsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListInsertSets(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + ) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 0 { + t.Fail() + } +} + +func TestInsertInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + insertSet = InsertSet{} + insertSets []InsertSet + ) + insertSet.ID = bson.NewObjectId() + insertSet.Name = "test" + err := insertsetDao.Insert(insertSet) + if err != nil { + t.Fail() + } + insertSets, err = insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 1 { + t.Fail() + } +} + +func TestUpsertInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + insertSet = InsertSet{} + ) + insertSet.ID = bson.NewObjectId() + insertSet.Name = "test2" + insertsetDao.Upsert(insertSet) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 2 { + t.Fail() + } +} + +func TestDeleteInsertSet(t *testing.T) { + var ( + insertsetDao = InsertSetDAO{Db: dao.TestDb} + ) + insertSets, err := insertsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, insertSet := range insertSets { + insertsetDao.Delete(insertSet) + } + insertSets, err = insertsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(insertSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/manufacturer.go b/go/pkg/tysc/manufacturer.go new file mode 100644 index 0000000..e4ccdc0 --- /dev/null +++ b/go/pkg/tysc/manufacturer.go @@ -0,0 +1,12 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Manufacturer defines the data model for TYSC manufacturers with id, and name. +type Manufacturer struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/manufacturer_dao.go b/go/pkg/tysc/manufacturer_dao.go new file mode 100644 index 0000000..b23be11 --- /dev/null +++ b/go/pkg/tysc/manufacturer_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ManufacturerDAO extends the type BaseDAO. +type ManufacturerDAO struct { + Db dao.BaseDAO +} + +const ( + // MANUFACTURERCOLLECTION defines the collection name for storing manufacturers. + MANUFACTURERCOLLECTION = "manufacturer" + // MANUFACTURERMODEL defines the name of the manufacturer data model. + MANUFACTURERMODEL = "kontor.tysc.manufacturer" +) + +// FindAll retrieves the list of manufacturers from the database. +func (m *ManufacturerDAO) FindAll() ([]Manufacturer, error) { + m.Db.Connect() + var manufacturers []Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Find(bson.M{"model": MANUFACTURERMODEL}).All(&manufacturers) + return manufacturers, err +} + +// FindByID returns a manufacturer with given id or returns the error. +func (m *ManufacturerDAO) FindByID(id string) (Manufacturer, error) { + m.Db.Connect() + var manufacturer Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&manufacturer) + return manufacturer, err +} + +// FindByName returns a manufacturer with given name or returns the error. +func (m *ManufacturerDAO) FindByName(name string) (Manufacturer, error) { + m.Db.Connect() + var manufacturer Manufacturer + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Find(bson.M{"name": name, "model": MANUFACTURERMODEL}).One(&manufacturer) + return manufacturer, err +} + +// Insert a manufacturer into database. +func (m *ManufacturerDAO) Insert(manufacturer Manufacturer) error { + m.Db.Connect() + manufacturer.Model = MANUFACTURERMODEL + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Insert(&manufacturer) + return err +} + +// Upsert a manufacturer into database. +func (m *ManufacturerDAO) Upsert(manufacturer Manufacturer) (*mgo.ChangeInfo, error) { + m.Db.Connect() + manufacturer.Model = MANUFACTURERMODEL + info, err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Upsert(bson.M{"name": manufacturer.Name}, &manufacturer) + return info, err +} + +// Update an existing manufacturer. +func (m *ManufacturerDAO) Update(manufacturer Manufacturer) error { + m.Db.Connect() + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).UpdateId(manufacturer.ID, &manufacturer) + return err +} + +// Delete an existing manufacturer. +func (m *ManufacturerDAO) Delete(manufacturer Manufacturer) error { + m.Db.Connect() + err := m.Db.MongoDb.C(MANUFACTURERCOLLECTION).Remove(&manufacturer) + return err +} diff --git a/go/pkg/tysc/manufacturer_test.go b/go/pkg/tysc/manufacturer_test.go new file mode 100644 index 0000000..9973beb --- /dev/null +++ b/go/pkg/tysc/manufacturer_test.go @@ -0,0 +1,102 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var manufacturerModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestManufacturerModel(t *testing.T) { + m := Manufacturer{} + if reflect.TypeOf(m).NumField() != len(manufacturerModelTestTable) { + t.Fail() + } + for index, testData := range manufacturerModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListManufacturers(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + ) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 0 { + t.Fail() + } +} + +func TestInsertManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + manufacturer = Manufacturer{} + manufacturers []Manufacturer + ) + manufacturer.ID = bson.NewObjectId() + manufacturer.Name = "test" + err := manufacturerDao.Insert(manufacturer) + if err != nil { + t.Fail() + } + manufacturers, err = manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 1 { + t.Fail() + } +} + +func TestUpsertManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + manufacturer = Manufacturer{} + ) + manufacturer.ID = bson.NewObjectId() + manufacturer.Name = "test2" + manufacturerDao.Upsert(manufacturer) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 2 { + t.Fail() + } +} + +func TestDeleteManufacturer(t *testing.T) { + var ( + manufacturerDao = ManufacturerDAO{Db: dao.TestDb} + ) + manufacturers, err := manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + for _, manufacturer := range manufacturers { + manufacturerDao.Delete(manufacturer) + } + manufacturers, err = manufacturerDao.FindAll() + if err != nil { + t.Fail() + } + if len(manufacturers) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/parallelset.go b/go/pkg/tysc/parallelset.go new file mode 100644 index 0000000..78a93e2 --- /dev/null +++ b/go/pkg/tysc/parallelset.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// ParallelSet defines the data model for TYSC parallel sets with id, name and manufacturer. +type ParallelSet struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Manufacturer bson.ObjectId `json:"manufacturer" bson:"manufacturer,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/parallelset_dao.go b/go/pkg/tysc/parallelset_dao.go new file mode 100644 index 0000000..0cadac3 --- /dev/null +++ b/go/pkg/tysc/parallelset_dao.go @@ -0,0 +1,82 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// ParallelSetDAO extends the type BaseDAO. +type ParallelSetDAO struct { + Db dao.BaseDAO +} + +const ( + // PARALLELSETCOLLECTION defines the collection name for storing parallel sets. + PARALLELSETCOLLECTION = "parallelSet" + // PARALLELSETMODEL defines the name of the parallel set data model. + PARALLELSETMODEL = "kontor.tysc.parallelSet" +) + +// FindAll retrieves the list of parallel sets from the database. +func (m *ParallelSetDAO) FindAll() ([]ParallelSet, error) { + m.Db.Connect() + var parallelSets []ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"model": PARALLELSETMODEL}).All(¶llelSets) + return parallelSets, err +} + +// FindByID returns a parallel set with given id or returns the error. +func (m *ParallelSetDAO) FindByID(id string) (ParallelSet, error) { + m.Db.Connect() + var parallelSet ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).FindId(bson.ObjectIdHex(id)).One(¶llelSet) + return parallelSet, err +} + +// FindByManufacturer returns a paralle set with given manufacturer or returns the error. +func (m *ParallelSetDAO) FindByManufacturer(manufacturer string) ([]ParallelSet, error) { + m.Db.Connect() + var parallelSets []ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"model": PARALLELSETMODEL, "manufacturer": bson.ObjectIdHex(manufacturer)}).All(¶llelSets) + return parallelSets, err +} + +// FindByName returns a parallel set with given name or returns the error. +func (m *ParallelSetDAO) FindByName(name string) (ParallelSet, error) { + m.Db.Connect() + var parallelSet ParallelSet + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Find(bson.M{"name": name, "model": PARALLELSETMODEL}).One(¶llelSet) + return parallelSet, err +} + +// Insert a parallel set into database. +func (m *ParallelSetDAO) Insert(parallelSet ParallelSet) error { + m.Db.Connect() + parallelSet.Model = PARALLELSETMODEL + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Insert(¶llelSet) + return err +} + +// Upsert a parallel set into database. +func (m *ParallelSetDAO) Upsert(parallelSet ParallelSet) (*mgo.ChangeInfo, error) { + m.Db.Connect() + parallelSet.Model = PARALLELSETMODEL + info, err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Upsert(bson.M{"name": parallelSet.Name}, ¶llelSet) + return info, err +} + +// Update an existing parallel set. +func (m *ParallelSetDAO) Update(parallelSet ParallelSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).UpdateId(parallelSet.ID, ¶llelSet) + return err +} + +// Delete an existing parallel set. +func (m *ParallelSetDAO) Delete(parallelSet ParallelSet) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PARALLELSETCOLLECTION).Remove(¶llelSet) + return err +} diff --git a/go/pkg/tysc/parallelset_test.go b/go/pkg/tysc/parallelset_test.go new file mode 100644 index 0000000..e0a9c2b --- /dev/null +++ b/go/pkg/tysc/parallelset_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var parallelsetModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Manufacturer", "string"}, + {"Model", "string"}, +} + +func TestParallelSetModel(t *testing.T) { + m := ParallelSet{} + if reflect.TypeOf(m).NumField() != len(parallelsetModelTestTable) { + t.Fail() + } + for index, testData := range parallelsetModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListParallelSets(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + ) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 0 { + t.Fail() + } +} + +func TestInsertParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + parallelSet = ParallelSet{} + parallelSets []ParallelSet + ) + parallelSet.ID = bson.NewObjectId() + parallelSet.Name = "test" + err := parallelsetDao.Insert(parallelSet) + if err != nil { + t.Fail() + } + parallelSets, err = parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 1 { + t.Fail() + } +} + +func TestUpsertParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + parallelSet = ParallelSet{} + ) + parallelSet.ID = bson.NewObjectId() + parallelSet.Name = "test2" + parallelsetDao.Upsert(parallelSet) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 2 { + t.Fail() + } +} + +func TestDeleteParallelSet(t *testing.T) { + var ( + parallelsetDao = ParallelSetDAO{Db: dao.TestDb} + ) + parallelSets, err := parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + for _, parallelSet := range parallelSets { + parallelsetDao.Delete(parallelSet) + } + parallelSets, err = parallelsetDao.FindAll() + if err != nil { + t.Fail() + } + if len(parallelSets) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/player.go b/go/pkg/tysc/player.go new file mode 100644 index 0000000..055efe6 --- /dev/null +++ b/go/pkg/tysc/player.go @@ -0,0 +1,13 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Player defines the data model for TYSC players with id, first and lastname. +type Player struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Firstname string `json:"firstname" bson:"firstname"` + Lastname string `json:"lastname" bson:"lastname"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/player_dao.go b/go/pkg/tysc/player_dao.go new file mode 100644 index 0000000..ff60c37 --- /dev/null +++ b/go/pkg/tysc/player_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PlayerDAO extends the type BaseDAO. +type PlayerDAO struct { + Db dao.BaseDAO +} + +const ( + // PLAYERCOLLECTION defines the collection name for storing players. + PLAYERCOLLECTION = "player" + // PLAYERMODEL defines the name of the player data model. + PLAYERMODEL = "kontor.tysc.player" +) + +// FindAll retrieves the list of players from the database. +func (m *PlayerDAO) FindAll() ([]Player, error) { + m.Db.Connect() + var players []Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Find(bson.M{"model": PLAYERMODEL}).All(&players) + return players, err +} + +// FindByID returns a player with given id or returns the error. +func (m *PlayerDAO) FindByID(id string) (Player, error) { + m.Db.Connect() + var player Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&player) + return player, err +} + +// FindByLastName returns a player with given last name or returns the error. +func (m *PlayerDAO) FindByLastName(lastname string) (Player, error) { + m.Db.Connect() + var player Player + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Find(bson.M{"lastname": lastname, "model": PLAYERMODEL}).One(&player) + return player, err +} + +// Insert a player into database. +func (m *PlayerDAO) Insert(player Player) error { + m.Db.Connect() + player.Model = PLAYERMODEL + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Insert(&player) + return err +} + +// Upsert a player into database. +func (m *PlayerDAO) Upsert(player Player) (*mgo.ChangeInfo, error) { + m.Db.Connect() + player.Model = PLAYERMODEL + info, err := m.Db.MongoDb.C(PLAYERCOLLECTION).Upsert(bson.M{"lastname": player.Lastname}, &player) + return info, err +} + +// Update an existing player. +func (m *PlayerDAO) Update(player Player) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PLAYERCOLLECTION).UpdateId(player.ID, &player) + return err +} + +// Delete an existing player. +func (m *PlayerDAO) Delete(player Player) error { + m.Db.Connect() + err := m.Db.MongoDb.C(PLAYERCOLLECTION).Remove(&player) + return err +} diff --git a/go/pkg/tysc/player_test.go b/go/pkg/tysc/player_test.go new file mode 100644 index 0000000..e839ee9 --- /dev/null +++ b/go/pkg/tysc/player_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var playerModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Firstname", "string"}, + {"Lastname", "string"}, + {"Model", "string"}, +} + +func TestPlayerModel(t *testing.T) { + m := Player{} + if reflect.TypeOf(m).NumField() != len(playerModelTestTable) { + t.Fail() + } + for index, testData := range playerModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPlayers(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + ) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + if players != nil { + t.Fail() + } +} + +func TestInsertPlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + player = Player{} + players []Player + ) + player.ID = bson.NewObjectId() + player.Lastname = "test" + err := playerDao.Insert(player) + if err != nil { + t.Fail() + } + players, err = playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 1 { + t.Fail() + } +} + +func TestUpsertPlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + player = Player{} + ) + player.ID = bson.NewObjectId() + player.Lastname = "test2" + playerDao.Upsert(player) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 2 { + t.Fail() + } +} + +func TestDeletePlayer(t *testing.T) { + var ( + playerDao = PlayerDAO{Db: dao.TestDb} + ) + players, err := playerDao.FindAll() + if err != nil { + t.Fail() + } + for _, player := range players { + playerDao.Delete(player) + } + players, err = playerDao.FindAll() + if err != nil { + t.Fail() + } + if len(players) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/position.go b/go/pkg/tysc/position.go new file mode 100644 index 0000000..b52703a --- /dev/null +++ b/go/pkg/tysc/position.go @@ -0,0 +1,14 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Position defines the data model for TYSC positions with id, name, description and sport. +type Position struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Description string `json:"description" bson:"description"` + Sport bson.ObjectId `json:"sport" bson:"sport,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/position_dao.go b/go/pkg/tysc/position_dao.go new file mode 100644 index 0000000..4166ee0 --- /dev/null +++ b/go/pkg/tysc/position_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// PositionDAO extends the type BaseDAO. +type PositionDAO struct { + Db dao.BaseDAO +} + +const ( + // POSITIONCOLLECTION defines the collection name for storing positions. + POSITIONCOLLECTION = "position" + // POSITIONMODEL defines the name of the position data model. + POSITIONMODEL = "kontor.tysc.position" +) + +// FindAll retrieves the list of positions from the database. +func (m *PositionDAO) FindAll() ([]Position, error) { + m.Db.Connect() + var positions []Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Find(bson.M{"model": POSITIONMODEL}).All(&positions) + return positions, err +} + +// FindByID returns a position with given id or returns the error. +func (m *PositionDAO) FindByID(id string) (Position, error) { + m.Db.Connect() + var position Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&position) + return position, err +} + +// FindByName returns a position with given name or returns the error. +func (m *PositionDAO) FindByName(name string) (Position, error) { + m.Db.Connect() + var position Position + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Find(bson.M{"name": name, "model": POSITIONMODEL}).One(&position) + return position, err +} + +// Insert a position into database. +func (m *PositionDAO) Insert(position Position) error { + m.Db.Connect() + position.Model = POSITIONMODEL + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Insert(&position) + return err +} + +// Upsert a position into database. +func (m *PositionDAO) Upsert(position Position) (*mgo.ChangeInfo, error) { + m.Db.Connect() + position.Model = POSITIONMODEL + info, err := m.Db.MongoDb.C(POSITIONCOLLECTION).Upsert(bson.M{"name": position.Name}, &position) + return info, err +} + +// Update an existing position. +func (m *PositionDAO) Update(position Position) error { + m.Db.Connect() + err := m.Db.MongoDb.C(POSITIONCOLLECTION).UpdateId(position.ID, &position) + return err +} + +// Delete an existing position. +func (m *PositionDAO) Delete(position Position) error { + m.Db.Connect() + err := m.Db.MongoDb.C(POSITIONCOLLECTION).Remove(&position) + return err +} diff --git a/go/pkg/tysc/position_test.go b/go/pkg/tysc/position_test.go new file mode 100644 index 0000000..2b97105 --- /dev/null +++ b/go/pkg/tysc/position_test.go @@ -0,0 +1,104 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var positionModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Description", "string"}, + {"Sport", "string"}, + {"Model", "string"}, +} + +func TestPositionModel(t *testing.T) { + m := Position{} + if reflect.TypeOf(m).NumField() != len(positionModelTestTable) { + t.Fail() + } + for index, testData := range positionModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListPositions(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + ) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 0 { + t.Fail() + } +} + +func TestInsertPosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + position = Position{} + positions []Position + ) + position.ID = bson.NewObjectId() + position.Name = "test" + err := positionDao.Insert(position) + if err != nil { + t.Fail() + } + positions, err = positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 1 { + t.Fail() + } +} + +func TestUpsertPosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + position = Position{} + ) + position.ID = bson.NewObjectId() + position.Name = "test2" + positionDao.Upsert(position) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 2 { + t.Fail() + } +} + +func TestDeletePosition(t *testing.T) { + var ( + positionDao = PositionDAO{Db: dao.TestDb} + ) + positions, err := positionDao.FindAll() + if err != nil { + t.Fail() + } + for _, position := range positions { + positionDao.Delete(position) + } + positions, err = positionDao.FindAll() + if err != nil { + t.Fail() + } + if len(positions) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/routes.go b/go/pkg/tysc/routes.go new file mode 100644 index 0000000..e581db7 --- /dev/null +++ b/go/pkg/tysc/routes.go @@ -0,0 +1,22 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + + "github.com/gin-gonic/gin" +) + +// GetRoutes returns all routes for TYSC related data. +func GetRoutes(router *gin.Engine) { + tyscRoutes := router.Group("/tysc") + tyscRoutes.GET("/", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/sport", auth.EnsureLoggedIn(), showSportList) + tyscRoutes.GET("/position", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/team", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/player", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/manufacturer", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/cardset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/parallelset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/insertset", auth.EnsureLoggedIn(), showIndexPage) + tyscRoutes.GET("/card", auth.EnsureLoggedIn(), showIndexPage) +} diff --git a/go/pkg/tysc/sport.go b/go/pkg/tysc/sport.go new file mode 100644 index 0000000..ed5b8ef --- /dev/null +++ b/go/pkg/tysc/sport.go @@ -0,0 +1,12 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Sport defines the data model for TYSC sports with id, and name. +type Sport struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/sport_dao.go b/go/pkg/tysc/sport_dao.go new file mode 100644 index 0000000..e931f98 --- /dev/null +++ b/go/pkg/tysc/sport_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// SportDAO extends the type BaseDAO. +type SportDAO struct { + Db dao.BaseDAO +} + +const ( + // SPORTCOLLECTION defines the collection name for storing sports. + SPORTCOLLECTION = "sport" + // SPORTMODEL defines the name of the sport data model. + SPORTMODEL = "kontor.tysc.sport" +) + +// FindAll retrieves the list of sports from the database. +func (m *SportDAO) FindAll() ([]Sport, error) { + m.Db.Connect() + var sports []Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).Find(bson.M{"model": SPORTMODEL}).All(&sports) + return sports, err +} + +// FindByID returns a sport with given id or returns the error. +func (m *SportDAO) FindByID(id string) (Sport, error) { + m.Db.Connect() + var sport Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&sport) + return sport, err +} + +// FindByName returns a sport with given name or returns the error. +func (m *SportDAO) FindByName(name string) (Sport, error) { + m.Db.Connect() + var sport Sport + err := m.Db.MongoDb.C(SPORTCOLLECTION).Find(bson.M{"name": name, "model": SPORTMODEL}).One(&sport) + return sport, err +} + +// Insert a sport into database. +func (m *SportDAO) Insert(sport Sport) error { + m.Db.Connect() + sport.Model = SPORTMODEL + err := m.Db.MongoDb.C(SPORTCOLLECTION).Insert(&sport) + return err +} + +// Upsert a sport into database. +func (m *SportDAO) Upsert(sport Sport) (*mgo.ChangeInfo, error) { + m.Db.Connect() + sport.Model = SPORTMODEL + info, err := m.Db.MongoDb.C(SPORTCOLLECTION).Upsert(bson.M{"name": sport.Name}, &sport) + return info, err +} + +// Update an existing sport. +func (m *SportDAO) Update(sport Sport) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SPORTCOLLECTION).UpdateId(sport.ID, &sport) + return err +} + +// Delete an existing sport. +func (m *SportDAO) Delete(sport Sport) error { + m.Db.Connect() + err := m.Db.MongoDb.C(SPORTCOLLECTION).Remove(&sport) + return err +} diff --git a/go/pkg/tysc/sport_test.go b/go/pkg/tysc/sport_test.go new file mode 100644 index 0000000..0aaf1c1 --- /dev/null +++ b/go/pkg/tysc/sport_test.go @@ -0,0 +1,102 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var sportModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Model", "string"}, +} + +func TestSport(t *testing.T) { + m := Sport{} + if reflect.TypeOf(m).NumField() != len(sportModelTestTable) { + t.Fail() + } + for index, testData := range sportModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListSports(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + ) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 0 { + t.Fail() + } +} + +func TestInsertSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + sport = Sport{} + sports []Sport + ) + sport.ID = bson.NewObjectId() + sport.Name = "test" + err := sportDao.Insert(sport) + if err != nil { + t.Fail() + } + sports, err = sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 1 { + t.Fail() + } +} + +func TestUpsertSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + sport = Sport{} + ) + sport.ID = bson.NewObjectId() + sport.Name = "test2" + sportDao.Upsert(sport) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 2 { + t.Fail() + } +} + +func TestDeleteSport(t *testing.T) { + var ( + sportDao = SportDAO{Db: dao.TestDb} + ) + sports, err := sportDao.FindAll() + if err != nil { + t.Fail() + } + for _, sport := range sports { + sportDao.Delete(sport) + } + sports, err = sportDao.FindAll() + if err != nil { + t.Fail() + } + if len(sports) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/team.go b/go/pkg/tysc/team.go new file mode 100644 index 0000000..167a2e5 --- /dev/null +++ b/go/pkg/tysc/team.go @@ -0,0 +1,14 @@ +package tysc + +import ( + "gopkg.in/mgo.v2/bson" +) + +// Team defines the data model for TYSC teams with id, name and sport. +type Team struct { + ID bson.ObjectId `json:"_id" bson:"_id,omitempty"` + Name string `json:"name" bson:"name"` + Shortname string `json:"shortname" bson:"shortname"` + Sport bson.ObjectId `json:"sport" bson:"sport,omitempty"` + Model string `json:"model" bson:"model,omitempty"` +} diff --git a/go/pkg/tysc/team_dao.go b/go/pkg/tysc/team_dao.go new file mode 100644 index 0000000..911763e --- /dev/null +++ b/go/pkg/tysc/team_dao.go @@ -0,0 +1,74 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + + mgo "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +// TeamDAO extends the type BaseDAO. +type TeamDAO struct { + Db dao.BaseDAO +} + +const ( + // TEAMCOLLECTION defines the collection name for storing teams. + TEAMCOLLECTION = "team" + // TEAMMODEL defines the name of the team data model. + TEAMMODEL = "kontor.tysc.team" +) + +// FindAll retrieves the list of cards from the database. +func (m *TeamDAO) FindAll() ([]Team, error) { + m.Db.Connect() + var teams []Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).Find(bson.M{"model": TEAMMODEL}).All(&teams) + return teams, err +} + +// FindByID returns a team with given id or returns the error. +func (m *TeamDAO) FindByID(id string) (Team, error) { + m.Db.Connect() + var team Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).FindId(bson.ObjectIdHex(id)).One(&team) + return team, err +} + +// FindByName returns a team with given name or returns the error. +func (m *TeamDAO) FindByName(name string) (Team, error) { + m.Db.Connect() + var team Team + err := m.Db.MongoDb.C(TEAMCOLLECTION).Find(bson.M{"name": name, "model": TEAMMODEL}).One(&team) + return team, err +} + +// Insert a team into database +func (m *TeamDAO) Insert(team Team) error { + m.Db.Connect() + team.Model = TEAMMODEL + err := m.Db.MongoDb.C(TEAMCOLLECTION).Insert(&team) + return err +} + +// Upsert a team into database +func (m *TeamDAO) Upsert(team Team) (*mgo.ChangeInfo, error) { + m.Db.Connect() + team.Model = TEAMMODEL + info, err := m.Db.MongoDb.C(TEAMCOLLECTION).Upsert(bson.M{"name": team.Name}, &team) + return info, err +} + +// Update an existing team. +func (m *TeamDAO) Update(team Team) error { + m.Db.Connect() + err := m.Db.MongoDb.C(TEAMCOLLECTION).UpdateId(team.ID, &team) + return err +} + +// Delete an existing team. +func (m *TeamDAO) Delete(team Team) error { + m.Db.Connect() + err := m.Db.MongoDb.C(TEAMCOLLECTION).Remove(&team) + return err +} diff --git a/go/pkg/tysc/team_test.go b/go/pkg/tysc/team_test.go new file mode 100644 index 0000000..79dd539 --- /dev/null +++ b/go/pkg/tysc/team_test.go @@ -0,0 +1,103 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "reflect" + "testing" + + "gopkg.in/mgo.v2/bson" +) + +var teamModelTestTable = []struct { + name string + typeName string +}{ + {"Id", "string"}, + {"Name", "string"}, + {"Shortname", "string"}, + {"Sport", "string"}, + {"Model", "string"}, +} + +func TestTeamModel(t *testing.T) { + m := Team{} + if reflect.TypeOf(m).NumField() != len(teamModelTestTable) { + t.Fail() + } + for index, testData := range teamModelTestTable { + givenType := reflect.TypeOf(m).Field(index).Type.Kind().String() + if givenType != testData.typeName { + t.Fail() + } + } +} + +func TestListTeams(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + ) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + if teams != nil { + t.Fail() + } +} + +func TestInsertTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + team = Team{} + teams []Team + ) + team.ID = bson.NewObjectId() + err := teamDao.Insert(team) + if err != nil { + t.Fail() + } + teams, err = teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 1 { + t.Fail() + } +} + +func TestUpsertTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + team = Team{} + ) + team.ID = bson.NewObjectId() + team.Name = "test2" + teamDao.Upsert(team) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 2 { + t.Fail() + } +} + +func TestDeleteTeam(t *testing.T) { + var ( + teamDao = TeamDAO{Db: dao.TestDb} + ) + teams, err := teamDao.FindAll() + if err != nil { + t.Fail() + } + for _, team := range teams { + teamDao.Delete(team) + } + teams, err = teamDao.FindAll() + if err != nil { + t.Fail() + } + if len(teams) != 0 { + t.Fail() + } +} diff --git a/go/pkg/tysc/views.go b/go/pkg/tysc/views.go new file mode 100644 index 0000000..0d9f233 --- /dev/null +++ b/go/pkg/tysc/views.go @@ -0,0 +1,22 @@ +package tysc + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/dao" + "gitlab.thpeetz.de/kontor/kontor-go/pkg/util" + "net/http" + + "github.com/gin-gonic/gin" +) + +func showIndexPage(c *gin.Context) { + util.Render(c, gin.H{"title": "TradeYourSportsCards", "payload": nil}, "tysc/index.html") +} + +func showSportList(c *gin.Context) { + var dao = SportDAO{Db: dao.KontorDb} + if sports, err := dao.FindAll(); err == nil { + util.Render(c, gin.H{"title": "TYSC Sport List", "payload": sports}, "tysc/sports.html") + } else { + c.AbortWithError(http.StatusNotFound, err) + } +} diff --git a/go/pkg/util/render.go b/go/pkg/util/render.go new file mode 100644 index 0000000..b786b4c --- /dev/null +++ b/go/pkg/util/render.go @@ -0,0 +1,39 @@ +package util + +import ( + "gitlab.thpeetz.de/kontor/kontor-go/pkg/auth" + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + // SaveAction defines label of button. + SaveAction = "Save" + // AddAction defines label of button. + AddAction = "Add" + // DeleteAction defines label of button. + DeleteAction = "Delete" +) + +// Render one of HTML, JSON or CSV based on the 'Accept' header of the request +// If the header doesn't specify this, HTML is rendered, provided that +// the template name is present +func Render(c *gin.Context, data gin.H, templateName string) { + auth.SetSessionData(c, data) + switch c.Request.Header.Get("Accept") { + case "application/json": + c.JSON(http.StatusOK, data["payload"]) + case "application/xml": + c.XML(http.StatusOK, data["payload"]) + default: + c.HTML(http.StatusOK, templateName, data) + } +} + +// ShowIndexPage render the index page of Kontor web application. +func ShowIndexPage(c *gin.Context) { + // Call the render function with the name of the template to render + //log.Printf("Context: %v", c) + Render(c, gin.H{"title": "Kontor", "payload": nil}, "kontor/index.html") +} diff --git a/go/sonar-project.properties b/go/sonar-project.properties new file mode 100644 index 0000000..b06f378 --- /dev/null +++ b/go/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.projectKey=kontor_kontor-go_AX-cQT62rXuu6JVRvr-z +sonar.qualitygate.wait=true diff --git a/go/templates/comics/artists.html b/go/templates/comics/artists.html new file mode 100644 index 0000000..f49ebbc --- /dev/null +++ b/go/templates/comics/artists.html @@ -0,0 +1,24 @@ + +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} +

+ +
+ + + + {{range .payload }} + + {{end}} +
Comic Artists
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} diff --git a/go/templates/comics/comic.html b/go/templates/comics/comic.html new file mode 100644 index 0000000..8184605 --- /dev/null +++ b/go/templates/comics/comic.html @@ -0,0 +1,34 @@ +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Title }} + + {{ else }} + + {{ end }} +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} diff --git a/go/templates/comics/comics.html b/go/templates/comics/comics.html new file mode 100644 index 0000000..aeca37f --- /dev/null +++ b/go/templates/comics/comics.html @@ -0,0 +1,24 @@ + +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + + +
+ + + + {{range .payload }} + + {{end}} +
List of Comics
Name
{{.Title}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} diff --git a/go/templates/comics/menu.html b/go/templates/comics/menu.html new file mode 100644 index 0000000..6fec416 --- /dev/null +++ b/go/templates/comics/menu.html @@ -0,0 +1,38 @@ +{{ define "comics/menu.html" }} + +{{ end }} diff --git a/go/templates/comics/publisher.html b/go/templates/comics/publisher.html new file mode 100644 index 0000000..fe38dcd --- /dev/null +++ b/go/templates/comics/publisher.html @@ -0,0 +1,32 @@ +{{ define "comics/publisher.html" }} +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/comics/publishers.html b/go/templates/comics/publishers.html new file mode 100644 index 0000000..d691cdc --- /dev/null +++ b/go/templates/comics/publishers.html @@ -0,0 +1,25 @@ + +{{ define "comics/publishers.html" }} +{{ template "header.html" .}} +{{ template "comics/menu.html" .}} + + +
+ + + + {{range .payload }} + + {{end}} +
Comic Publishers
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/kontor/admin-menu.html b/go/templates/kontor/admin-menu.html new file mode 100644 index 0000000..d5738a2 --- /dev/null +++ b/go/templates/kontor/admin-menu.html @@ -0,0 +1,34 @@ + diff --git a/go/templates/kontor/admin.html b/go/templates/kontor/admin.html new file mode 100644 index 0000000..9dd19ad --- /dev/null +++ b/go/templates/kontor/admin.html @@ -0,0 +1,11 @@ + +{{ define "kontor/admin.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/kontor/create-user.html b/go/templates/kontor/create-user.html new file mode 100644 index 0000000..1a67785 --- /dev/null +++ b/go/templates/kontor/create-user.html @@ -0,0 +1,40 @@ +{{ define "kontor/create-user.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/data-upload.html b/go/templates/kontor/data-upload.html new file mode 100644 index 0000000..b2f77f2 --- /dev/null +++ b/go/templates/kontor/data-upload.html @@ -0,0 +1,29 @@ +{{ define "kontor/data-upload.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+ +
+
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/footer.html b/go/templates/kontor/footer.html new file mode 100644 index 0000000..3760315 --- /dev/null +++ b/go/templates/kontor/footer.html @@ -0,0 +1,26 @@ + + + + + + + diff --git a/go/templates/kontor/header.html b/go/templates/kontor/header.html new file mode 100644 index 0000000..bcf20aa --- /dev/null +++ b/go/templates/kontor/header.html @@ -0,0 +1,17 @@ + + + + + + + {{ .title }} + + + + + + + + + + diff --git a/go/templates/kontor/index.html b/go/templates/kontor/index.html new file mode 100644 index 0000000..4e23ba7 --- /dev/null +++ b/go/templates/kontor/index.html @@ -0,0 +1,17 @@ + +{{ define "kontor/index.html" }} + +{{ template "header.html" .}} + +{{ template "menu.html" . }} + +
+ {{ if .InfoMessage}} +

{{.InfoMessage}}

+ {{end}} +
+ + +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/kontor/login-successful.html b/go/templates/kontor/login-successful.html new file mode 100644 index 0000000..46e07fa --- /dev/null +++ b/go/templates/kontor/login-successful.html @@ -0,0 +1,12 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +
+ You have successfully logged in. +
+ + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/login.html b/go/templates/kontor/login.html new file mode 100644 index 0000000..83fce69 --- /dev/null +++ b/go/templates/kontor/login.html @@ -0,0 +1,35 @@ + + + +{{ template "header.html" .}} +{{ template "menu.html" . }} + +

Login

+ + +
+
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} + +
+
+ + +
+
+ + +
+ +
+
+
+ + + +{{ template "footer.html" .}} diff --git a/go/templates/kontor/menu.html b/go/templates/kontor/menu.html new file mode 100644 index 0000000..b52a358 --- /dev/null +++ b/go/templates/kontor/menu.html @@ -0,0 +1,36 @@ + diff --git a/go/templates/kontor/user-detail.html b/go/templates/kontor/user-detail.html new file mode 100644 index 0000000..983a614 --- /dev/null +++ b/go/templates/kontor/user-detail.html @@ -0,0 +1,59 @@ +{{ define "kontor/user-detail.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Username }} + + {{ else }} + + {{ end }} +
+
+
+ + {{ if .payload.Firstname }} + + {{ else }} + + {{ end }} +
+
+ + {{ if .payload.Lastname }} + + {{ else }} + + {{ end }} +
+
+
+ + +
+
+ + +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/kontor/users.html b/go/templates/kontor/users.html new file mode 100644 index 0000000..8aef51c --- /dev/null +++ b/go/templates/kontor/users.html @@ -0,0 +1,38 @@ +{{ define "kontor/users.html" }} +{{ template "header.html" .}} +{{ template "admin-menu.html" .}} + + +
+ + + + + + + + + {{range .payload }} + + + + + {{ if .IsAdmin}} + + {{end}} + {{ if not .IsAdmin }} + + {{end}} + + {{end}} +
Registered users of Kontor
UsernameFirst NameLast NameAdministrator
{{.Username}}{{.Firstname}}{{.Lastname}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/library/authors.html b/go/templates/library/authors.html new file mode 100644 index 0000000..781ec26 --- /dev/null +++ b/go/templates/library/authors.html @@ -0,0 +1,25 @@ + +{{ define "library/authors.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Autoren
Name
{{.Name}}
+
+ Add entry +
+
+ + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/books.html b/go/templates/library/books.html new file mode 100644 index 0000000..456cb82 --- /dev/null +++ b/go/templates/library/books.html @@ -0,0 +1,25 @@ + +{{ define "library/books.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" . }} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Bücher
Title
{{.Title}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/library/menu.html b/go/templates/library/menu.html new file mode 100644 index 0000000..2e6257f --- /dev/null +++ b/go/templates/library/menu.html @@ -0,0 +1,38 @@ +{{ define "library/menu.html" }} + +{{ end }} diff --git a/go/templates/library/publisher.html b/go/templates/library/publisher.html new file mode 100644 index 0000000..c0dae5e --- /dev/null +++ b/go/templates/library/publisher.html @@ -0,0 +1,32 @@ +{{ define "library/publisher.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ {{ if .ErrorTitle}} +

+ {{.ErrorTitle}}: {{.ErrorMessage}} +

+ {{end}} +
+
+ + {{ if .payload.Name }} + + {{ else }} + + {{ end }} +
+ + + {{ if eq .action "Save" }} + + {{ end }} +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/library/publishers.html b/go/templates/library/publishers.html new file mode 100644 index 0000000..a166167 --- /dev/null +++ b/go/templates/library/publishers.html @@ -0,0 +1,24 @@ + +{{ define "library/publishers.html" }} +{{ template "header.html" .}} +{{ template "library/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
Liste der Verlage
Name
{{.Name}}
+
+ Add entry +
+
+ +{{ template "footer.html" .}} +{{end}} diff --git a/go/templates/office/index.html b/go/templates/office/index.html new file mode 100644 index 0000000..3932a29 --- /dev/null +++ b/go/templates/office/index.html @@ -0,0 +1,10 @@ + +{{ define "office/index.html" }} +{{ template "header.html" .}} +{{ template "office/menu.html" . }} + +

Home Office

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/office/menu.html b/go/templates/office/menu.html new file mode 100644 index 0000000..ddcd03d --- /dev/null +++ b/go/templates/office/menu.html @@ -0,0 +1,38 @@ +{{ define "office/menu.html" }} + +{{ end }} diff --git a/go/templates/tradingcards/index.html b/go/templates/tradingcards/index.html new file mode 100644 index 0000000..6911e17 --- /dev/null +++ b/go/templates/tradingcards/index.html @@ -0,0 +1,10 @@ + +{{ define "tradingcards/index.html" }} +{{ template "header.html" .}} +{{ template "tradingcards/menu.html" . }} + +

Trading Cards

+ +{{ template "footer.html" .}} + +{{ end }} diff --git a/go/templates/tradingcards/menu.html b/go/templates/tradingcards/menu.html new file mode 100644 index 0000000..524080c --- /dev/null +++ b/go/templates/tradingcards/menu.html @@ -0,0 +1,38 @@ +{{ define "tradingcards/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/index.html b/go/templates/tysc/index.html new file mode 100644 index 0000000..b1d135d --- /dev/null +++ b/go/templates/tysc/index.html @@ -0,0 +1,16 @@ +{{ define "tysc/index.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" . }} + +{{ template "footer.html" .}} +{{ end }} diff --git a/go/templates/tysc/menu.html b/go/templates/tysc/menu.html new file mode 100644 index 0000000..53a8e9e --- /dev/null +++ b/go/templates/tysc/menu.html @@ -0,0 +1,38 @@ +{{ define "tysc/menu.html" }} + +{{ end }} diff --git a/go/templates/tysc/sports.html b/go/templates/tysc/sports.html new file mode 100644 index 0000000..cf65f25 --- /dev/null +++ b/go/templates/tysc/sports.html @@ -0,0 +1,28 @@ +{{ define "tysc/sports.html" }} +{{ template "header.html" .}} +{{ template "tysc/menu.html" .}} + +
+ + + + {{range .payload }} + + {{end}} +
List of Amertican Sports
Name
{{.Name}}
+
+ Add entry +
+
+{{ template "footer.html" .}} +{{ end }} diff --git a/go/tysc-20041010-1819.sql b/go/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/go/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/java-ee/ComicsImpl/build.gradle b/java-ee/ComicsImpl/build.gradle new file mode 100644 index 0000000..9aa2749 --- /dev/null +++ b/java-ee/ComicsImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Comics', 'Implementation-Version': version + } +} diff --git a/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/ComicsImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/ComicsImpl/config/findbugs/findbugs.xml b/java-ee/ComicsImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/ComicsImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java new file mode 100644 index 0000000..346f363 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistDao.java @@ -0,0 +1,19 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ArtistEntity; + +@Local +public interface ArtistDao { + + public ArtistEntity getById(Long id); + + public List findByIds(List ids); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..0f18e6c --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ArtistImpl.java @@ -0,0 +1,45 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.comics.entity.ArtistEntity; + +@Stateless(name = "ArtistDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ArtistImpl implements ArtistDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ArtistEntity getById(Long id) { + Query q = em.createNamedQuery(""); + q.setParameter("id", id); + ArtistEntity entity = (ArtistEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + em.remove(entity); + } +} \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java new file mode 100644 index 0000000..957bc01 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicDao.java @@ -0,0 +1,23 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.PublisherEntity; + +@Local +public interface ComicDao { + public ComicEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public ComicEntity assignPublisher(ComicEntity comic, PublisherEntity publisher); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java new file mode 100644 index 0000000..d2adb99 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/dal/ComicImpl.java @@ -0,0 +1,62 @@ +package com.peetz.comics.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.PublisherEntity; + +@Stateless(name = "ComicDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ComicImpl implements ComicDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ComicEntity getById(Long id) { + Query q = em.createNamedQuery("Comic.findById"); + q.setParameter("id", id); + ComicEntity entity = (ComicEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List findByTitle(String title) { + Query q = em.createNamedQuery("Comic.findByTitle"); + q.setParameter("title", title); + @SuppressWarnings("unchecked") + List resultList = q.getResultList(); + return resultList; + } + + @Override + public ComicEntity assignPublisher(ComicEntity comic, + PublisherEntity publisher) { + // TODO Auto-generated method stub + return null; + } + + @Override + public ComicEntity store(ComicEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..57e0474 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ArtistEntity.java @@ -0,0 +1,69 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java new file mode 100644 index 0000000..4475647 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/ComicEntity.java @@ -0,0 +1,90 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) + +@Entity +@Table(name="COMIC") +public class ComicEntity +{ + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + public PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java new file mode 100644 index 0000000..fbda2c4 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/IssueEntity.java @@ -0,0 +1,80 @@ +package com.peetz.comics.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9252ea8 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/PublisherEntity.java @@ -0,0 +1,48 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name="PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..cffec70 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/StoryArcEntity.java @@ -0,0 +1,56 @@ +package com.peetz.comics.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT a from StoryArcEntity as a"), + @NamedQuery(name="StoryArc.findByArtist", query="SELECT a from StoryArcEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name="STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..8f25d13 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/entity/VolumeEntity.java @@ -0,0 +1,37 @@ +package com.peetz.comics.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java new file mode 100644 index 0000000..523d230 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicService.java @@ -0,0 +1,34 @@ +package com.peetz.comics.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; + +@Local +public interface ComicService { + public Collection getAllComics(); + + public Collection getAllPublisher(); + + public Collection getAllArtists(); + + public Collection getAllIssuesForComic(ComicEntity comic); + + public Collection getAllStoryArcs(); + + public void addStoryArc(String title); + + public void addPublisher(String name); + + public ComicEntity addComic(String title); + + public PublisherEntity getPublisherById(String nodeValue); + + public void assignPublisher(ComicEntity comic, PublisherEntity publisher); +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java new file mode 100644 index 0000000..5f3e222 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/ComicServiceImpl.java @@ -0,0 +1,92 @@ +package com.peetz.comics.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="ComicService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ComicServiceImpl implements ComicService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllComics() { + Query query = em.createNamedQuery("Comic.findAll"); + ArrayList comicList = new ArrayList(query.getResultList()); + return comicList; + } + + @Override + public Collection getAllPublisher() { + Query query = em.createNamedQuery("Publisher.findAll"); + @SuppressWarnings("unchecked") + ArrayList publisherList = new ArrayList(query.getResultList()); + return publisherList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllArtists() { + Query query = em.createNamedQuery("Artist.findAll"); + ArrayList artistList = new ArrayList(query.getResultList()); + return artistList; + } + + @Override + public Collection getAllIssuesForComic(ComicEntity comic) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getAllStoryArcs() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void addStoryArc(String title) { + // TODO Auto-generated method stub + + } + + @Override + public void addPublisher(String name) { + // TODO Auto-generated method stub + + } + + @Override + public ComicEntity addComic(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public PublisherEntity getPublisherById(String nodeValue) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void assignPublisher(ComicEntity comic, PublisherEntity publisher) { + // TODO Auto-generated method stub + + } + +} diff --git a/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java new file mode 100644 index 0000000..a2d9305 --- /dev/null +++ b/java-ee/ComicsImpl/src/main/java/com/peetz/comics/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.comics.service; \ No newline at end of file diff --git a/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java b/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java new file mode 100644 index 0000000..05f38c1 --- /dev/null +++ b/java-ee/ComicsImpl/src/test/java/com/peetz/comics/service/ComicServiceImplTest.java @@ -0,0 +1,195 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.comics.service; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import java.util.Collection; +import javax.ejb.embeddable.EJBContainer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.Ignore; + +/** + * + * @author TPEETZ + */ +public class ComicServiceImplTest { + + private static ComicService instance; + private static EJBContainer container; + + public ComicServiceImplTest() { + } + + @BeforeClass + public static void setUpClass() throws Exception { + container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + instance = (ComicService)container.getContext().lookup("java:global/main/ComicService"); + } + + @AfterClass + public static void tearDownClass() { + container.close(); + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of getAllComics method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllComics() throws Exception { + System.out.println("getAllComics"); + Collection expResult = null; + Collection result = instance.getAllComics(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllPublisher() throws Exception { + System.out.println("getAllPublisher"); + Collection expResult = null; + Collection result = instance.getAllPublisher(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllArtists method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllArtists() throws Exception { + System.out.println("getAllArtists"); + Collection expResult = null; + Collection result = instance.getAllArtists(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllIssuesForComic method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllIssuesForComic() throws Exception { + System.out.println("getAllIssuesForComic"); + ComicEntity comic = null; + Collection expResult = null; + Collection result = instance.getAllIssuesForComic(comic); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getAllStoryArcs method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetAllStoryArcs() throws Exception { + System.out.println("getAllStoryArcs"); + Collection expResult = null; + Collection result = instance.getAllStoryArcs(); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addStoryArc method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddStoryArc() throws Exception { + System.out.println("addStoryArc"); + String title = ""; + instance.addStoryArc(title); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddPublisher() throws Exception { + System.out.println("addPublisher"); + String name = ""; + instance.addPublisher(name); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of addComic method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAddComic() throws Exception { + System.out.println("addComic"); + String title = ""; + ComicEntity expResult = null; + ComicEntity result = instance.addComic(title); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of getPublisherById method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testGetPublisherById() throws Exception { + System.out.println("getPublisherById"); + String nodeValue = ""; + PublisherEntity expResult = null; + PublisherEntity result = instance.getPublisherById(nodeValue); + assertEquals(expResult, result); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of assignPublisher method, of class ComicServiceImpl. + */ + @Ignore + @Test + public void testAssignPublisher() throws Exception { + System.out.println("assignPublisher"); + ComicEntity comic = null; + PublisherEntity publisher = null; + instance.assignPublisher(comic, publisher); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } +} diff --git a/java-ee/ComicsWeb/build.gradle b/java-ee/ComicsWeb/build.gradle new file mode 100644 index 0000000..30e1d2e --- /dev/null +++ b/java-ee/ComicsWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':ComicsImpl') +} diff --git a/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java b/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java new file mode 100644 index 0000000..d21494c --- /dev/null +++ b/java-ee/ComicsWeb/src/main/java/com/peetz/comics/view/ComicView.java @@ -0,0 +1,36 @@ +package com.peetz.comics.view; + +import com.peetz.comics.service.ComicService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="ComicView") +@RequestScoped +public class ComicView implements Serializable { + + private static final Logger LOG = Logger.getLogger(ComicView.class.getName()); + + @EJB + private ComicService comicService; + + private static final long serialVersionUID = -8261128991042235283L; + + public ComicView() { + LOG.info("ComicView created"); + } + + public Integer getComicsNumber() { + return comicService.getAllComics().size(); + } + + public Integer getPublisherNumber() { + return comicService.getAllPublisher().size(); + } +} diff --git a/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp new file mode 100644 index 0000000..9328510 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistAdd.jsp @@ -0,0 +1,64 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp new file mode 100644 index 0000000..3007a9a --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/artistList.jsp b/java-ee/ComicsWeb/src/main/webapp/artistList.jsp new file mode 100644 index 0000000..1414873 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/artistList.jsp @@ -0,0 +1,95 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic artist list + + + + <%-- set the header --%> + + + + + + <%-- check if publisher exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + <%-- print out the edit and delete link for each artist --%> + + + + + + <%-- end interate --%> + + <%-- if publishers cannot be found display a text --%> + + + + + + + +
Artist name  
No artists available
EditDelete
No artists found.
+
+ <%-- add and back to menu button --%> + Add a new artist +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp new file mode 100644 index 0000000..c623224 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
Publisher:
Completed:
Current Order:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp new file mode 100644 index 0000000..ea1cae9 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicEdit.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
Publisher:
Completed:
Current Order:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + + + + + + + + + + + + + + + + + + + +
Issues  
No issues available
EditDelete
No issues available
Add issue
+
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comicList.jsp b/java-ee/ComicsWeb/src/main/webapp/comicList.jsp new file mode 100644 index 0000000..4f2cc06 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comicList.jsp @@ -0,0 +1,99 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic list + + + + <%-- set the header --%> + + + + + + + + <%-- check if book exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if books cannot be found display a text --%> + + + + + + + +
Comic namePublisherOrder  
No comics available
EditDelete
No comicss found.
+
+ <%-- add and back to menu button --%> + Add a new comic +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/comics.xhtml b/java-ee/ComicsWeb/src/main/webapp/comics.xhtml new file mode 100644 index 0000000..506f71f --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/comics.xhtml @@ -0,0 +1,51 @@ + + + + + + + + + Comics Application + + + + + + + + + + + + + + + + + + + + + + + + + +
Kontor Manager
+ Kontor
+ Comics
+ Library
+ Medien
+ TradingCards +
+

Kontor Manager

+ + +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/ComicsWeb/src/main/webapp/index.jsp b/java-ee/ComicsWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..5825223 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/index.jsp @@ -0,0 +1,35 @@ + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + + +
Comic Manager
test +

Comic Manager

+ Show the comic list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp new file mode 100644 index 0000000..91aaae1 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/issueAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Comic:
Number:
Author:
Read:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp new file mode 100644 index 0000000..0ea25cb --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/issueEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + +
Number:
Author:
Read:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp new file mode 100644 index 0000000..85f220a --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherAdd.jsp @@ -0,0 +1,64 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp new file mode 100644 index 0000000..a8eb56d --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherEdit.jsp @@ -0,0 +1,66 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + +
Name:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp b/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp new file mode 100644 index 0000000..b0ef596 --- /dev/null +++ b/java-ee/ComicsWeb/src/main/webapp/publisherList.jsp @@ -0,0 +1,95 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Comic Application + + + + + + + + + + + + + + + + + + + + + + + + +
Liste der Comics
+ <% out.println(com.peetz.comics.navigation.MenuLinks.getInstance().toString()); %> + +

Comic Manager

+ Show the comic list + + + + <%-- set the header --%> + + + + + + <%-- check if publisher exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the book informations --%> + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if publishers cannot be found display a text --%> + + + + + + + +
Publisher name  
No publishers available
EditDelete
No publishers found.
+
+ <%-- add and back to menu button --%> + Add a new publisher +   + Back to menu + + + + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/DVDs.csv b/java-ee/DVDs.csv new file mode 100644 index 0000000..1b8df87 --- /dev/null +++ b/java-ee/DVDs.csv @@ -0,0 +1,391 @@ +Title +(T)Raumschiff Surprise - Periode 1 +7 Zwerge - Männer allein im Wald +A Beatiful Mind +About Schmidt +Akte X: Der Film +Aladdin +"Alias - Die Agentin +Vol. 1" +"Alias - Die Agentin +Vol. 2" +"Alias - Die Agentin +Vol. 3" +Alles tanzt nach meiner Pfeife +American Beauty +American History X +American Pie +American Pie 2 +American Pie Jetzt wird geheiratet +American Psycho +An jedem verdammten Sonntag +Angeklagt +Antz +Apollo 13 +Arac Attack +Arachnophobia +Arlington Road +Asso +Atlantis Das Geheimnis der verlorenen Stadt +Austin Powers +Austin Powers Goldständer +Austin Powers Spion in geheimer Missionarsstellung +"Avanti, Avanti" +Backdraft +Bad Taste +Bang Boom Bang +Bärenbrüder +Being John Malkovich +"Berlin, Berlin - Staffel 1" +"Berlin, Berlin - Staffel 2" +"Berlin, Berlin - Staffel 3" +"Berlin, Berlin - Staffel 4" +Bernard & Bianca +Beverly Hills Cop +Beverly Hills Cop II +Beverly Hills Cop III +Black Hawk Down +Blow +Blues Brothers +Blues Brothers 2000 +Bowfingers grosse Nummer +Braveheart +Brust oder Keule +Caddyshack +Chasing Amy +Cheech & Chong's The Corsican Brothers +Cheech & Chong im Dauerstress +Cheech & Chong Jetzt raucht überhaupt nichts mehr +Cheech & Chong Viel Rauch um Nichts +Cheech & Chongs Heisse Träume +Cocktail für eine Leiche +ConAir +Daredevil +Dark City +Das Appartment +Das Fenster zum Hof +Das fünfte Element +Das grosse Krabbeln +Das Jesus Video +Das kleine Arschloch +Das Krokodil und sein Nilpferd +Das Netz +Das Schweigen der Lämmer +Das Wunder von Bern +Der Boss +Der Club der toten Dichter +Der dritte Mann +Der Glückspilz +Der Hauch des Todes +Der kleine Horrorladen +Der Knochenjäger +Der Mann mit dem goldenen Colt +"Der Mann, der zuviel wußte" +Der Millionenfinger +Der Mondmann +Der Morgen stirbt nie +Der Name der Rose +Der Pate 1-3 +Der Puppenspieler +Der Querkopf +Der Schatzplanet +"Der Spion, der mich liebte" +Der talentierte Mr. Ripley +Der Totmacher +Der Wixxer +Diamantenfieber +"Dick und Doof erben eine Insel, Atoll K" +Didi - der Doppelgänger +Didi - Der Experte +Didi - Der Schnüffler +Didi - und die Rache der Enterbten +Didi auf vollen Touren +Die Ärzte Unplugged Rock'n' Roll Realschule +Die Einsteiger +"Die Geister, die ich rief" +Die Glücksjäger +Die Glücksritter +Die große Schlacht des Don Camillo +Die Harald Schmidt Show - Best Of Vol. 2 +Die Harald Schmidt Show - Best of +Die Jury +Die Liga der aussergewöhnlichen Gentlemen +Die Monster AG +Die Muppets erobern Manhattan +Die Muppets Weihnachtsgeschichte +Die nackte Kanone +Die nackte Kanone 2 1/2 +Die nackte Kanone 33 1/3 +Die neun Pforten +Die rechte und die linke Hand des Teufels +Die Truman Show +Die unglaubliche Reise in einem verrückten Flugzeug +Die unglaubliche Reise in einem verrückten Raumschiff +Die Unglaublichen +Die Verurteilten +Die Welt ist nicht genug +Die Wiege der Sonne +Diese Zwei sind nicht zu fassen +Dogma +Don Camillo und Peppone +Don Camillos Rückkehr +Dr. Dolittle 1+2 +Dragonheart +Drei Amigos +Ein Fisch namens Wanda +Ein Fisch namens Wanda +Ein irrer Typ +Ein Ticket für Zwei +Eine Leiche zum Dessert +"Eine schrecklich nette Familie +Dritte Staffel" +"Eine schrecklich nette Familie +Erste Staffel" +"Eins, Zwei, Drei" +Eiskalte Engel +El Dorado +Elektra +Es war einmal in Amerika +Evolution +Ey Mann - Wo is' mein Auto? +Faceoff - Im Körper des Feindes +Fahrenheit 9/11 +Falsches Spiel mit Roger Rabbit +Feuerball +Findet Nemo +Flammendes Inferno +Fletcher's Visionen +Fluch der Karibik +Forrest Gump +Four Rooms +Foxy Brown +Freddy vs. Jason +Freeze - Alptraum Nachtwache +Frenzy +Frequency +Fröhliche Ostern +From Dusk Till Dawn +From Dusk Till Dawn 2 +"From Dusk Till Dawn 3 +The Hangman's Daughter" +From Hell +Galaxy Quest +Gangs of New York +Gegen jede Regel +Geld oder Leber +Genosse Don Camillo +Ghost Ship +Ghostbusters 2 +Girls United +Glauben ist Alles! +God's Army +God's Army 3 +GoldenEye +Goldfinger +Good Bye Lenin! +Good Morning Vietnam +Good Will Hunting +Gottes Werk und Teufels Beitrag +Grosse Erwartungen +Hannibal +Harald Schmidt Best of Vol. 1+2 + Golden Goals +Helden aus der zweiten Reihe +Hercules +Hero +Hochwürden Don Camillo +Höllentour - Die Tour der Helden +Hot Shots 1 + 2 +Hulk +"I, Robot" +Ice Age +Im Angesicht des Todes +Im Geheimdienst Ihrer Majestät +Immer Ärger mit Bernie +In 80 Tagen um die Welt Teil 1 +In 80 Tagen um die Welt Teil 2 +In tödlicher Mission +Independence Day +Irma La Douce +Jabberwocky +Jackie Brown +Jagd auf einen Unsichtbaren +Jagd auf Roter Oktober +James Bond jagt Dr. No +JFK John F. Kennedy - Tatort Dallas +Johnny English +Jumanji +Kentucky Fried Movie +Kill Bill Vol. 1 +Kill Bill Vol. 2 +"Knight Moves +Ein mörderisches Spiel" +König der Fischer +La Boum +La Boum 2 +Leben und Sterben lassen +Liebesgrüße aus Moskau +Lizenz zum Töten +Lola rennt +Lost in Space +Lost In Translation +Louis und seine außerirdischen Kohlköpfe +Luther +Mallrats +Man lebt nur zweimal +Marillion Christmas in the Chapel +Marillion Live From Loreley +Marillion shot in the dark +Marnie +Master & Commander +Maverick +"MexiCollection +El Mariachi +Desperado +Irgendwann in Mexico" +MIB +MIB 2 +Missing +"Montys enzyklopythonia (Das Leben des Brian, Die Ritter der Kokosnuss, Der Sinn des Lebens)" +Moonraker - Streng Geheim +Mörderischer Vorsprung +Mr. Bean 1 +Mr. Bean 2 +Mr. Bean 3 +Muppets aus dem All +Muppets Die Schatzinsel +Mystic River +Nick Knatterton Teil 1 +Nick Knatterton Teil 2 +Nightmare Before Christmas +Nightwish - end of innocence +Nur 48 Stunden +"O Brother, Where Art Thou?" +Ocean's Eleven +Ocean's Twelve +Octopussy +"Onkel Paul, die große Pflaume" +Open Range +Oscar +Panic Room +Perdita Durango +Peter Gabriel Growing Up Live +Peter Gabriel Secret World Live +Platoon +Pulp Fiction +Rat Race Der nackte Wahnsinn +Reservoir Dogs +Resident Evil +Richy Guitar +Romeo Must Die +Roter Drache +Sag niemals nie +Scary Movie +Scary Movie 2 +Scharfe Kurven für Madame +Schiffsmeldungen +Schlappe Bullen beissen nicht +Scooby-Doo +Scream +Scream 2 +Scrubs: Die Anfänger - Die komplette erste Staffel (4 DVDs) +Scrubs: Die Anfänger - Die komplette zweite Staffel (4 DVDs) +Scrubs: Die Anfänger - Die komplette dritte Staffel (4 DVDs) +Shaft +Shakespeare in Love +Shang-High Noon +Shrek +Shrek 2 +Sideways +Sin City +Sin Eater +Sleepers +Sleepy Hollow +Small Soldiers +snatch Schweine und Diamanten +Solo für Zwei +Sonnenallee +South Park Der Film +Space Cowboys +Spaceballs +Speed Teil 1 + Teil 2 +Sphere +Spider-Man 2 +Spiel mir das Lied vom Tod +Stakeout II +Star Trek 1 +Star Trek 10 Nemesis +Star Trek 2 Der Zorn des Khan +Star Trek 3 Auf der Such nach Mr. Spock +Star Trek 4 Zurück in die Gegenwart +Star Trek 5 Am Rande des Universums +Star Trek 6 Das unentdeckte Land +Star Trek 7 Treffen der Generationen +Star Trek 8 Der erste Kontakt +Star Trek 9 Der Aufstand +Star Wars - Bonusmaterial +Star Wars - Clone Wars Vol. 1 +Star Wars - Clone Wars Vol. 2 +Star Wars - Episode 2 +Star Wars - Episode 3 - Die Rache der Sith +Star Wars - Episode IV - Eine neue Hoffnung +Star Wars - Episode V - Das Imperium schlägt zurück +Star Wars - Episode VI - Die Rückkehr der Jedi-Ritter +Stigmata +Stirb an einem anderen Tag +Stirb langsam 1+2 +Sumo Bruno +Tanz der Teufel +Tanz der Vampire +Terminator 2 Tag der Abrechnung +Terminator 3 Rebellion der Maschinen +The Abyss +The Art of War +The Big Lebowski +The Core +The Crow +The Day After Tomorrow +The Fog Nebel des Grauens +The Game +The Green Mile +The Rock +The Scorpion King +The Time Machine +Three Kings +Tiger & Dragon +Tomb Raider +Tomb Raider Die Wiege des Lebens +Topas +Toy Story +Toy Story 2 +Traffic Macht des Kartells +Troja +Tron +Twister +U-Turn +UHF +Und dann kam Polly +under suspicion Mörderisches Spiel +Underworld +Van Helsing +Verlockende Falle +Verrückt nach Mary +Verrückt nach mehr Mary +Vertigo +Vier Fäuste für ein Halleluja +Vier Fäuste gegen Rio +Volcano +Volker Pispers Live +Wayne's World +Wayne's World 2 +Wilde Kreaturen +X-Men +X-Men 2 +Zoolander +Zurück in die Zukunft Trilogie Boxset +Zwei Asse trumpfen auf +Zwei außer Rand und Band +Zwei bärenstarke Typen +Zwei Himmelhunde auf dem Weg zur Hölle +Zwei hinreissend verdorbene Schurken +Zwei Nasen tanken Super +Zwei sind nicht zu bremsen diff --git a/java-ee/Jenkinsfile b/java-ee/Jenkinsfile new file mode 100644 index 0000000..2f64305 --- /dev/null +++ b/java-ee/Jenkinsfile @@ -0,0 +1,15 @@ +node { + stage "Checkout" + checkout scm + stage 'Prepare Gradle build' + sh "chmod +x gradlew" + stage 'Stage Build' + sh "./gradlew --no-daemon clean build -x findbugsMain -x findbugsTest -x test" + stage 'Archive Artifacts' + archiveArtifacts allowEmptyArchive: true, artifacts: '**/build/reports/*/*.xml', defaultExcludes: false, onlyIfSuccessful: true + archiveArtifacts allowEmptyArchive: true, artifacts: '**/reports/*/*.html' + junit allowEmptyResults: true, testResults: '**/build/test-results/*.xml' + //step([$class: 'CheckStylePublisher', pattern: '**/build/reports/checkstyle/*.xml']) + //step([$class: 'FindBugsPublisher', pattern: '**/build/reports/findbugs/*.xml']) +} + diff --git a/java-ee/KontorApp/build.gradle b/java-ee/KontorApp/build.gradle new file mode 100644 index 0000000..437481b --- /dev/null +++ b/java-ee/KontorApp/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'java' +apply plugin: 'application' + +dependencies { + compile 'org.hibernate:hibernate-core:4.3.8.Final' + compile 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + compile 'org.hsqldb:hsqldb:2.3.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +mainClassName = 'com.ibtp.kontor.KontorApp' + +jar { + manifest { + attributes 'Implementation-Title': 'Kontor Application', 'Implementation-Version': version, 'Main-Class': mainClassName + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java new file mode 100644 index 0000000..b87a849 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorApp.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class KontorApp { + + private KontorGUI mainframe; + + public KontorApp() { + mainframe = new KontorGUI(this); + + mainframe.setVisible(true); + } + + public void exitApplication() { + System.exit(0); + } + + public static void main(String[] args) { + new KontorApp(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java new file mode 100644 index 0000000..40a4b2d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/KontorGUI.java @@ -0,0 +1,69 @@ +package com.ibtp.kontor; + + +import com.ibtp.kontor.comics.view.ComicsMenu; +import com.ibtp.kontor.library.view.LibraryMenu; +import com.ibtp.kontor.tradingcards.view.TradingCardsMenu; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Created by TPEETZ on 11.02.2015. + */ +public class KontorGUI extends javax.swing.JFrame { + + KontorApp application; + JMenuBar menuBar; + JMenu menuFile; + JMenuItem menuFileExit; + + JMenuItem menuFileStart = new JMenuItem(); + + JMenu menuHelp; + JMenuItem menuHelpAbout; + + public KontorGUI(KontorApp kontorApp) { + application = kontorApp; + initComponents(); + } + + private void initComponents() { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 300, Short.MAX_VALUE) + ); + pack(); + setTitle("Kontor Application"); + createMainMenu(); + //createToolBar(); + } + + private void createMainMenu() { + menuBar = new JMenuBar(); + menuFile = new JMenu("File"); + menuFileExit = new JMenuItem("Exit"); + menuHelp = new JMenu("Help"); + menuHelpAbout = new JMenuItem("About"); + setJMenuBar(menuBar); + menuBar.add(menuFile); + menuFile.add(menuFileExit); + menuBar.add(new ComicsMenu()); + menuBar.add(new LibraryMenu()); + menuBar.add(new TradingCardsMenu()); + menuBar.add(menuHelp); + menuHelp.add(menuHelpAbout); + menuFileExit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + application.exitApplication(); + } + }); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java new file mode 100644 index 0000000..00c4b01 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +interface ArtistDao { + + public ArtistEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public ArtistEntity addArtist(String name); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..1f5c604 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class ArtistImpl extends BaseImpl implements ArtistDao { + + public ArtistImpl() {} + + @Override + public ArtistEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Artist.findById"); + query.setParameter("id", id); + return (ArtistEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Artist.findAll"); + return query.getResultList(); + + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Artist.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public ArtistEntity addArtist(String name) { + ArtistEntity artist = new ArtistEntity(name); + artist = store(artist); + return artist; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java new file mode 100644 index 0000000..c77fb89 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface ComicDao { + + public ComicEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public ComicEntity addComic(String title); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java new file mode 100644 index 0000000..ecbad88 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImpl extends BaseImpl implements ComicDao { + + @Override + public ComicEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Comic.findById"); + query.setParameter("id", id); + return (ComicEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Comic.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Comic.findAll"); + return query.getResultList(); + } + + @Override + public ComicEntity addComic(String title) { + ComicEntity comicEntity = new ComicEntity(); + comicEntity.setTitle(title); + store(comicEntity); + return comicEntity; + } + + @Override + public ComicEntity store(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java new file mode 100644 index 0000000..49a5ab2 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface IssueDao { + public IssueEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByNumber(String number); + + public Collection findAll(); + + public IssueEntity store(IssueEntity entity); + + public void delete(IssueEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java new file mode 100644 index 0000000..8f70845 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImpl extends BaseImpl implements IssueDao { + + @Override + public IssueEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Issue.findById"); + query.setParameter("id", id); + return (IssueEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByNumber(String number) { + Query query = getEntityManager().createNamedQuery("Issue.findByNumber"); + query.setParameter("number", number); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Issue.findAll"); + return query.getResultList(); + } + + @Override + public IssueEntity store(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java new file mode 100644 index 0000000..0722522 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface PublisherDao { + + public PublisherEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PublisherEntity addPublisher(String name); + + public PublisherEntity store(PublisherEntity entity); + + public void delete(PublisherEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java new file mode 100644 index 0000000..d911c4d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImpl extends BaseImpl implements PublisherDao { + + @Override + public PublisherEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Publisher.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Publisher.findAll"); + return query.getResultList(); + } + + @Override + public PublisherEntity addPublisher(String name) { + PublisherEntity publisher = new PublisherEntity(name); + store(publisher); + return publisher; + } + + @Override + public PublisherEntity store(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java new file mode 100644 index 0000000..40761fe --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface StoryArcDao { + + public StoryArcEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public StoryArcEntity store(StoryArcEntity entity); + + public void delete(StoryArcEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java new file mode 100644 index 0000000..3a39271 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImpl extends BaseImpl implements StoryArcDao { + + @Override + public StoryArcEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("StoryArc.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("StoryArc.findAll"); + return query.getResultList(); + } + + @Override + public StoryArcEntity store(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java new file mode 100644 index 0000000..26759bd --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface VolumeDao { + + public VolumeEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public VolumeEntity store(VolumeEntity entity); + + public void delete(VolumeEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java new file mode 100644 index 0000000..bb20a4a --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImpl extends BaseImpl implements VolumeDao { + + @Override + public VolumeEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Volume.findById"); + query.setParameter("id", id); + return (VolumeEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Volume.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Volume.findAll"); + return query.getResultList(); + } + + @Override + public VolumeEntity store(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..50ede83 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 16.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + public ArtistEntity(String name) { + setName(name); + } + + public ArtistEntity() {} + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } + + @Override + public String toString() { + return "Artist[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java new file mode 100644 index 0000000..2d21c10 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java @@ -0,0 +1,81 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) +@Entity +@Table(name = "COMIC") +public class ComicEntity { + + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + private PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java new file mode 100644 index 0000000..93a7662 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; + +/** + * Created by thomas on 18.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9aec40e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java @@ -0,0 +1,47 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name = "PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + public PublisherEntity() {} + + public PublisherEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..e3ea22b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT s from StoryArcEntity as s"), + @NamedQuery(name="StoryArc.findByTitle", query="SELECT s from StoryArcEntity as s WHERE s.title = :title") +}) + +@Entity +@Table(name = "STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..6f896ad --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java @@ -0,0 +1,40 @@ +package com.ibtp.kontor.comics.entity; + + +import javax.persistence.*; + +/** + * Created by TPEETZ on 19.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Volume.findAll", query="SELECT v from VolumeEntity as v"), + @NamedQuery(name="Volume.findByTitle", query="SELECT v from VolumeEntity as v WHERE v.title = :title") +}) + +@Entity +@Table(name = "VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java new file mode 100644 index 0000000..5201a11 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.comics.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class ComicsMenu extends JMenu { + + public ComicsMenu() { + super("Comics"); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java new file mode 100644 index 0000000..fa22722 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/BaseImpl.java @@ -0,0 +1,21 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class BaseImpl { + + protected BaseImpl() { + Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + logger.info("BaseImpl started"); + } + + protected EntityManager getEntityManager() { + return DatabaseManager.getDatabase().getEntityManager(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java new file mode 100644 index 0000000..67dbda9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/Database.java @@ -0,0 +1,11 @@ +package com.ibtp.kontor.dal; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public interface Database { + + public EntityManager getEntityManager(); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java new file mode 100644 index 0000000..0ccc25d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 22.01.2015. + */ +public class DatabaseManager { + + private static Database database; + private static Logger logger = LoggerFactory.getLogger(DatabaseManager.class.getName()); + + public static Database getDatabase() { + logger.info("return " + database.toString()); + return database; + } + + public static void setDatabase(Database database) { + logger.info("set " + database.toString()); + DatabaseManager.database = database; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java new file mode 100644 index 0000000..37cc68b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java @@ -0,0 +1,103 @@ +package com.ibtp.kontor.dal; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class LocalDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + + private LocalDatabase() { + logger.info("LocalDatabase started"); + } + + private static void assureDatabaseRunning() { + if (LocalDatabase.server == null) { + LocalDatabase.startDatabase(); + } + } + + private static void startDatabase() { + Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + logger.info("startDatabase as kontor in hsqldb_databases/kontor"); + LocalDatabase.server = new Server(); + LocalDatabase.server.setAddress("localhost"); + LocalDatabase.server.setDatabaseName(0, "kontor"); + LocalDatabase.server.setDatabasePath(0, "file:hsqldb_databases/kontor"); + LocalDatabase.server.setPort(2345); + LocalDatabase.server.setTrace(true); + LocalDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + private static EntityManagerFactory getFactory() { + if (LocalDatabase.factory == null) { + LocalDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return LocalDatabase.factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalDatabase.server.toString(); + } + return LocalDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java new file mode 100644 index 0000000..f9cd144 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface ArticleDao { + + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findAll(); + + public List findByTitle(String title); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java new file mode 100644 index 0000000..ada92d9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.ArticleEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImpl extends BaseImpl implements ArticleDao { + + @Override + public ArticleEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Article.findById"); + query.setParameter("id", id); + return (ArticleEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Article.findAll"); + //noinspection unchecked + return query.getResultList(); + } + + @Override + public List findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Article.findByTitle"); + query.setParameter("title", title); + //noinspection unchecked + return query.getResultList(); + } + + public ArticleEntity addArticle(String title) { + ArticleEntity entity = new ArticleEntity(title); + store(entity); + return entity; + } + + @Override + public ArticleEntity store(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java new file mode 100644 index 0000000..85f4c83 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java @@ -0,0 +1,25 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface AuthorDao { + + public AuthorEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public List findAll(); + + public AuthorEntity addAuthor(String name); + + public AuthorEntity store(AuthorEntity entity); + + public void delete(AuthorEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java new file mode 100644 index 0000000..573d7d9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.AuthorEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImpl extends BaseImpl implements AuthorDao { + + public AuthorImpl() {} + + @Override + public AuthorEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Author.findById"); + query.setParameter("id", id); + return (AuthorEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Author.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Author.findAll"); + return query.getResultList(); + } + + @Override + public AuthorEntity addAuthor(String name) { + AuthorEntity author = new AuthorEntity(name); + store(author); + return author; + } + + @Override + public AuthorEntity store(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java new file mode 100644 index 0000000..d89a1b7 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface BookDao { + + public BookEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BookEntity store(BookEntity entity); + + public void delete(BookEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java new file mode 100644 index 0000000..63f0ab1 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImpl extends BaseImpl implements BookDao { + + @Override + public BookEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public BookEntity store(BookEntity entity) { + return null; + } + + @Override + public void delete(BookEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java new file mode 100644 index 0000000..f123e69 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface FileDao { + + public FileEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public FileEntity store(FileEntity entity); + + public void delete(FileEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java new file mode 100644 index 0000000..1af2900 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImpl extends BaseImpl implements FileDao { + + @Override + public FileEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public FileEntity store(FileEntity entity) { + return null; + } + + @Override + public void delete(FileEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java new file mode 100644 index 0000000..81da92b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface TitleDao { + + public TitleEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public TitleEntity store(TitleEntity entity); + + public void delete(TitleEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java new file mode 100644 index 0000000..410c2c5 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImpl extends BaseImpl implements TitleDao { + + @Override + public TitleEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public TitleEntity store(TitleEntity entity) { + return null; + } + + @Override + public void delete(TitleEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java new file mode 100644 index 0000000..d61eda5 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a"), + @NamedQuery(name="Article.findByTitle", query="SELECT a from ArticleEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name = "ARTICLE") +public class ArticleEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + public ArticleEntity() {} + + public ArticleEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java new file mode 100644 index 0000000..63a6f4b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Author.findAll", query="SELECT a from AuthorEntity as a"), + @NamedQuery(name="Author.findById", query="SELECT a from AuthorEntity as a WHERE a.id = :id"), + @NamedQuery(name="Author.findByName", query="SELECT a from AuthorEntity as a WHERE a.name = :name") +}) +@Entity +@Table(name="AUTHOR") +public class AuthorEntity { + + private Long id; + + private String name; + + private Collection books = new ArrayList(); + + private Collection articles = new ArrayList(); + + public AuthorEntity() {} + + public AuthorEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java new file mode 100644 index 0000000..b57705c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java @@ -0,0 +1,96 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "BOOK") +public class BookEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + public BookEntity() {} + + public BookEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java new file mode 100644 index 0000000..05f4577 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "FILE") +public class FileEntity { + + private Long id; + + private String title; + + public FileEntity() {} + + public FileEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java new file mode 100644 index 0000000..ba25eb9 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "TITLE") +public class TitleEntity { + + private Long id; + + private String title; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java new file mode 100644 index 0000000..0b97e27 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.library.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class LibraryMenu extends JMenu { + + public LibraryMenu() { + super("Library"); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java new file mode 100644 index 0000000..29692d4 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface BaseSetDao { + + public BaseSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BaseSetEntity store(BaseSetEntity entity); + + public void delete(BaseSetEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java new file mode 100644 index 0000000..9e584a4 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BaseSetImpl extends BaseImpl implements BaseSetDao { + + @Override + public BaseSetEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("BaseSet.findById"); + query.setParameter("id", id); + return (BaseSetEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("BaseSet.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public BaseSetEntity store(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java new file mode 100644 index 0000000..c68790f --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface InsertDao { + + public InsertEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public InsertEntity store(InsertEntity entity); + + public void delete(InsertEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java new file mode 100644 index 0000000..598763c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImpl extends BaseImpl implements InsertDao { + + @Override + public InsertEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public InsertEntity store(InsertEntity entity) { + return null; + } + + @Override + public void delete(InsertEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..451d18b --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface ManufacturerDao { + + public ManufacturerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity manufacturer, BaseSetEntity baseSet); + + public ManufacturerEntity addManufacturer(String name); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..58c2fd6 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class ManufacturerImpl extends BaseImpl implements ManufacturerDao { + + @Override + public ManufacturerEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + return (ManufacturerEntity)q.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity addManufacturer(String name) { + ManufacturerEntity manufacturer = new ManufacturerEntity(name); + store(manufacturer); + return manufacturer; + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java new file mode 100644 index 0000000..c4d36ba --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface ParallelSetDao { + + public ParallelSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ParallelSetEntity store(ParallelSetEntity entity); + + public void delete(ParallelSetEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java new file mode 100644 index 0000000..cb604a0 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImpl extends BaseImpl implements ParallelSetDao { + + @Override + public ParallelSetEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public ParallelSetEntity store(ParallelSetEntity entity) { + return null; + } + + @Override + public void delete(ParallelSetEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java new file mode 100644 index 0000000..11d045e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PlayerDao { + + public PlayerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public PlayerEntity addPlayer(String name); + + public PlayerEntity store(PlayerEntity entity); + + public void delete(PlayerEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java new file mode 100644 index 0000000..c3d5104 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImpl extends BaseImpl implements PlayerDao { + + @Override + public PlayerEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public PlayerEntity addPlayer(String name) { + return null; + } + + @Override + public PlayerEntity store(PlayerEntity entity) { + return null; + } + + @Override + public void delete(PlayerEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java new file mode 100644 index 0000000..9970491 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PositionDao { + + public PositionEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PositionEntity addPosition(String name); + + public PositionEntity store(PositionEntity entity); + + public void delete(PositionEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java new file mode 100644 index 0000000..8aad906 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImpl extends BaseImpl implements PositionDao { + + @Override + public PositionEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Position.findById"); + q.setParameter("id", id); + return (PositionEntity)q.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query q = getEntityManager().createNamedQuery("Position.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public Collection findAll() { + Query q = getEntityManager().createNamedQuery("Position.findAll"); + return q.getResultList(); + } + + @Override + public PositionEntity addPosition(String name) { + PositionEntity position = new PositionEntity(name); + store(position); + return position; + } + + @Override + public PositionEntity store(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java new file mode 100644 index 0000000..25fb244 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface SportCardDao { + + public SportCardEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public SportCardEntity store(SportCardEntity entity); + + public void delete(SportCardEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java new file mode 100644 index 0000000..5939d2c --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImpl extends BaseImpl implements SportCardDao { + + @Override + public SportCardEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public SportCardEntity store(SportCardEntity entity) { + return null; + } + + @Override + public void delete(SportCardEntity entity) { + + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..f5ba51e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface SportDao { + public SportEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public SportEntity addSport(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..223236d --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class SportImpl extends BaseImpl implements SportDao { + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Sport.findAll"); + return query.getResultList(); + } + + @Override + public SportEntity addSport(String name) { + SportEntity sport = new SportEntity(name); + store(sport); + return sport; + } + + @Override + public SportEntity store(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java new file mode 100644 index 0000000..c2f9eff --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface TeamDao { + public TeamEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public TeamEntity addTeam(String name, SportEntity sport); + + public TeamEntity store(TeamEntity entity); + + public void delete(TeamEntity entity); +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java new file mode 100644 index 0000000..7fb8244 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class TeamImpl extends BaseImpl implements TeamDao { + + @Override + public TeamEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Team.findById"); + query.setParameter("id", id); + return (TeamEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Team.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Team.findAll"); + return query.getResultList(); + } + + @Override + public TeamEntity addTeam(String name, SportEntity sport) { + TeamEntity team = new TeamEntity(name); + team.setSport(sport); + store(team); + return team; + } + + @Override + public TeamEntity store(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..3111e98 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..ce59037 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..f83da2a --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,78 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + public ManufacturerEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..0ce0c69 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..7c19d70 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..3e575e0 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.*; + +@NamedQueries({ + @NamedQuery(name="Position.findAll", query="SELECT m from PositionEntity as m"), + @NamedQuery(name="Position.findByName", query="SELECT m from PositionEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + public PositionEntity() {} + + public PositionEntity(String name) { setName(name); } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..f71f722 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..8e2258e --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + public SportEntity() {} + + public SportEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } + + @Override + public String toString() { + return "Sport[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..8dbcb77 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + public TeamEntity() {} + + public TeamEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java new file mode 100644 index 0000000..f124056 --- /dev/null +++ b/java-ee/KontorApp/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.tradingcards.view; + +import javax.swing.*; + +/** + * Created by TPEETZ on 13.02.2015. + */ +public class TradingCardsMenu extends JMenu { + + public TradingCardsMenu() { + super("TradingCards"); + } +} diff --git a/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml b/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java-ee/KontorApp/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/main/resources/logback.xml b/java-ee/KontorApp/src/main/resources/logback.xml new file mode 100644 index 0000000..e8cff02 --- /dev/null +++ b/java-ee/KontorApp/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + c:/kontor.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + c:/kontor.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java new file mode 100644 index 0000000..146c854 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/CollectionTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.comics; + +import com.ibtp.kontor.comics.dal.PublisherImpl; +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.*; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @After + public void cleanup() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherEntities = publisherImpl.findAll(); + for (Iterator iterator = publisherEntities.iterator(); iterator.hasNext(); ) { + PublisherEntity next = iterator.next(); + publisherImpl.delete(next); + } + } + + @Test + public void testAddPublishers() { + String publisherName = "Bongo Comics"; + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("Bongo Comics"); + publisherImpl.addPublisher("Marvel"); + Collection publisherList = publisherImpl.findAll(); + Assert.assertEquals(2, publisherList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java new file mode 100644 index 0000000..4095fba --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java @@ -0,0 +1,50 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.*; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.*; + +import java.util.Collection; +import java.util.Iterator; + +public class ArtistImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testArtistAddAndDelete() { + String artistName = "testArtistAddAndDelete"; + ArtistImpl artistImpl = new ArtistImpl(); + artistImpl.addArtist(artistName); + Collection resultList = artistImpl.findByName(artistName); + Assert.assertNotNull(resultList); + Assert.assertTrue(resultList.size() > 0); + ArtistEntity artist = (ArtistEntity)(resultList.toArray()[0]); + artistImpl.delete(artist); + resultList = artistImpl.findByName(artistName); + Assert.assertNotNull(resultList); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testArtistFindAll() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + Assert.assertNotNull(artistList); + Assert.assertEquals(0, artistList.size()); + artistImpl.addArtist("testArtistFindAll1"); + artistImpl.addArtist("testArtistFindAll2"); + artistImpl.addArtist("testArtistFindAll3"); + artistList = artistImpl.findAll(); + Assert.assertNotNull(artistList); + Assert.assertEquals(3, artistList.size()); + for (Iterator iterator = artistList.iterator(); iterator.hasNext(); ) { + ArtistEntity next = iterator.next(); + artistImpl.delete(next); + } + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java new file mode 100644 index 0000000..eb494c9 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java @@ -0,0 +1,54 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testComicAddAndDelete() { + String comicTitle = "Comic1"; + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic(comicTitle); + Collection comicList = comicImpl.findByTitle(comicTitle); + Assert.assertNotNull(comicList); + Assert.assertEquals(1, comicList.size()); + comicImpl.delete((ComicEntity) comicList.toArray()[0]); + comicList = comicImpl.findByTitle(comicTitle); + Assert.assertNotNull(comicList); + Assert.assertEquals(0, comicList.size()); + } + + @Test + public void testComicFindAll() { + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic("Comic1"); + comicImpl.addComic("Comic2"); + comicImpl.addComic("Comic3"); + Collection comicList = comicImpl.findAll(); + Assert.assertNotNull(comicList); + Assert.assertEquals(3, comicList.size()); + for (Iterator iterator = comicList.iterator(); iterator.hasNext(); ) { + ComicEntity next = iterator.next(); + comicImpl.delete(next); + } + comicList = comicImpl.findAll(); + Assert.assertNotNull(comicList); + Assert.assertEquals(0, comicList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java new file mode 100644 index 0000000..4cf7cdc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testIssueAddAndDelete() { + String issueNumber = "42"; + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue = new IssueEntity(); + issue.setNumber(issueNumber); + issueImpl.store(issue); + Collection issueList = issueImpl.findByNumber(issueNumber); + Assert.assertNotNull(issueList); + Assert.assertEquals(1, issueList.size()); + issueImpl.delete(issue); + issueList = issueImpl.findByNumber(issueNumber); + Assert.assertNotNull(issueList); + Assert.assertEquals(0, issueList.size()); + } + + @Test + public void testIssueFindAll() { + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue1 = new IssueEntity(); + issue1.setNumber("issue1"); + IssueEntity issue2 = new IssueEntity(); + issue1.setNumber("issue2"); + IssueEntity issue3 = new IssueEntity(); + issue1.setNumber("issue3"); + issueImpl.store(issue1); + issueImpl.store(issue2); + issueImpl.store(issue3); + Collection issueList = issueImpl.findAll(); + Assert.assertNotNull(issueList); + Assert.assertEquals(3, issueList.size()); + for (Iterator iterator = issueList.iterator(); iterator.hasNext(); ) { + IssueEntity next = iterator.next(); + issueImpl.delete(next); + } + issueList = issueImpl.findAll(); + Assert.assertNotNull(issueList); + Assert.assertEquals(0, issueList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java new file mode 100644 index 0000000..9f459cc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java @@ -0,0 +1,50 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPublisherAddAndDelete() { + String publisherName = "testPublisherAddAndDelete"; + PublisherImpl publisherImpl = new PublisherImpl(); + PublisherEntity publisher = publisherImpl.addPublisher(publisherName); + Collection publisherList = publisherImpl.findByName(publisherName); + Assert.assertEquals(1, publisherList.size()); + publisherImpl.delete(publisher); + publisherList = publisherImpl.findByName(publisherName); + Assert.assertEquals(0, publisherList.size()); + } + + @Test + public void testPublisherFindAll() { + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("testDeletePublisher1"); + publisherImpl.addPublisher("testDeletePublisher2"); + publisherImpl.addPublisher("testDeletePublisher3"); + Collection publisherList = publisherImpl.findAll(); + Assert.assertEquals(3, publisherList.size()); + for (Iterator iterator = publisherList.iterator(); iterator.hasNext(); ) { + PublisherEntity next = iterator.next(); + publisherImpl.delete(next); + } + publisherList = publisherImpl.findAll(); + Assert.assertEquals(0, publisherList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java new file mode 100644 index 0000000..8df7b94 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testStoryArcAddAndDelete() { + String storyArcTtitle = "testStoryArcAddAndDelete"; + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc = new StoryArcEntity(); + storyArc.setTitle(storyArcTtitle); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(1, storyArcEntityCollection.size()); + storyArcImpl.delete(storyArc); + storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(0, storyArcEntityCollection.size()); + } + + @Test + public void testStoryArcFindAll() { + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc; + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll1"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll2"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll3"); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + Assert.assertNotNull(storyArcEntityCollection); + Assert.assertEquals(3, storyArcEntityCollection.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java new file mode 100644 index 0000000..e5c5b3a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testVolumeAddAndDelete() { + String volumeTitle = "testVolumeAddAndDelete"; + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume = new VolumeEntity(); + volume.setTitle(volumeTitle); + VolumeEntity volumeEntity = volumeImpl.store(volume); + Assert.assertNotNull(volumeEntity); + Assert.assertEquals(volumeTitle, volumeEntity.getTitle()); + Collection volumeList = volumeImpl.findByTitle(volumeTitle); + Assert.assertNotNull(volumeList); + Assert.assertEquals(1, volumeList.size()); + VolumeEntity result = (VolumeEntity)volumeList.toArray()[0]; + Assert.assertEquals(volume, result); + volumeImpl.delete(result); + volumeList = volumeImpl.findByTitle(volumeTitle); + Assert.assertEquals(0, volumeList.size()); + } + + @Test + public void testVolumeFindAll() { + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume; + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll1"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll2"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll3"); + volumeImpl.store(volume); + Collection volumeList = volumeImpl.findAll(); + Assert.assertNotNull(volumeList); + Assert.assertEquals(3, volumeList.size()); + for (Iterator iterator = volumeList.iterator(); iterator.hasNext(); ) { + VolumeEntity next = iterator.next(); + volumeImpl.delete(next); + } + volumeList = volumeImpl.findAll(); + Assert.assertNotNull(volumeList); + Assert.assertEquals(0, volumeList.size()); + } + +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java new file mode 100644 index 0000000..6351468 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.dal; + +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Method; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class DataAccessLayerTest { + + public void findTests(String packageName, String entityName) { + String testClassName = packageName + entityName + "ImplTest"; + Class testClass; + try { + testClass = Class.forName(testClassName); + Method addAndDelete = testClass.getMethod("test" + entityName + "AddAndDelete"); + Method findAll = testClass.getMethod("test" + entityName + "FindAll"); + } catch (ClassNotFoundException e) { + Assert.fail("Class " + testClassName + " missing"); + } catch (NoSuchMethodException e) { + Assert.fail("Test method for class " + testClassName + " missing"); + } + } + + @Test + public void testFindComicTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Artist", "Comic", "Issue", "Publisher", "StoryArc", "Volume"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.comics.dal.", testEntity); + } + } + + @Test + public void testFindLibraryTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Article", "Author", "Book", "File", "Title"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.library.dal.", testEntity); + } + } + + @Test + public void testFindTradingCardsTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"BaseSet", "Insert", "Manufacturer", "ParallelSet", "Player", "Position", "SportCard", "Sport", "Team"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.tradingcards.dal." , testEntity); + } + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java new file mode 100644 index 0000000..57f0a84 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/BookshelfTest.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookshelfTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthors() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java new file mode 100644 index 0000000..5fa268b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.library.entity.ArticleEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddArticle() { + String articleTitle = "testAddArticle"; + ArticleImpl articleImpl = new ArticleImpl(); + ArticleEntity article = articleImpl.addArticle(articleTitle); + Assert.assertNotNull(article); + List articleList = articleImpl.findByTitle(articleTitle); + Assert.assertEquals(1, articleList.size()); + } + + @Test + public void testArticleAddAndDelete() { + + } + + @Test + public void testArticleFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java new file mode 100644 index 0000000..f10caf6 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java @@ -0,0 +1,52 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.library.entity.AuthorEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthor() { + String authorName = "testAddAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + Assert.assertNotNull(author); + } + + @Test + public void testDeleteAuthor() { + String authorName = "testDeleteAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + Assert.assertNotNull(author); + List authorList = authorImpl.findByName(authorName); + Assert.assertEquals(1, authorList.size()); + authorImpl.delete(author); + authorList = authorImpl.findByName(authorName); + Assert.assertEquals(0, authorList.size()); + } + + @Test + public void testAuthorAddAndDelete() { + + } + + @Test + public void testAuthorFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java new file mode 100644 index 0000000..ba4c6d4 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBookAddAndDelete() { + + } + + @Test + public void testBookFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java new file mode 100644 index 0000000..a0afab6 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testFileAddAndDelete() { + + } + + @Test + public void testFileFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java new file mode 100644 index 0000000..e6f06bd --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTitleAddAndDelete() { + + } + + @Test + public void testTitleFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java new file mode 100644 index 0000000..d307051 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java @@ -0,0 +1,167 @@ +package com.ibtp.kontor.tradingcards; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.tradingcards.dal.TeamImpl; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.dal.SportImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + setupSports(); + } + + public void setupSports() { + SportImpl sportImpl = new SportImpl(); + SportEntity football = sportImpl.addSport("Football"); + setupFootballTeams(football); + SportEntity baseball = sportImpl.addSport("Baseball"); + setupFootballTeams(baseball); + SportEntity basketball = sportImpl.addSport("Basketball"); + setupBasketballTeams(basketball); + SportEntity hockey = sportImpl.addSport("Hockey"); + setupHockeyTeams(hockey); + } + + public void setupFootballTeams(SportEntity football) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Dallas Cowboys", football); + teamImpl.addTeam("New York Giants", football); + teamImpl.addTeam("Philadelphia Eagles", football); + teamImpl.addTeam("Arizona Cardinals", football); + teamImpl.addTeam("Washington Redskins", football); + teamImpl.addTeam("Detroit Lions", football); + teamImpl.addTeam("Minnesota Vikings", football); + teamImpl.addTeam("Green Bay Packers", football); + teamImpl.addTeam("Chicago Bears", football); + teamImpl.addTeam("Tampa Bay Buccaneers", football); + teamImpl.addTeam("San Francisco 49ers", football); + teamImpl.addTeam("New Orleans Saints", football); + teamImpl.addTeam("Atlanta Falcons", football); + teamImpl.addTeam("Los Angeles Rams", football); + teamImpl.addTeam("Buffalo Bills", football); + teamImpl.addTeam("Miami Dolphins", football); + teamImpl.addTeam("New York Jets", football); + teamImpl.addTeam("New England Patriots", football); + teamImpl.addTeam("Indianapolis Colts", football); + teamImpl.addTeam("Houston Oilers", football); + teamImpl.addTeam("Pittsburgh Steelers", football); + teamImpl.addTeam("Cleveland Browns", football); + teamImpl.addTeam("Kansas City Chiefs", football); + teamImpl.addTeam("Los Angeles Raiders", football); + teamImpl.addTeam("Denver Broncos", football); + teamImpl.addTeam("San Diego Chargers", football); + teamImpl.addTeam("Seattle Seahawks", football); + teamImpl.addTeam("Jacksonville Jaguars", football); + teamImpl.addTeam("Houston Texans", football); + } + + public void setupBaseballTeams(SportEntity baseball) { + TeamImpl teamImpl = new TeamImpl(); + } + + public void setupBasketballTeams(SportEntity basketball) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Houston Rockets", basketball); + teamImpl.addTeam("San Antonio Spurs", basketball); + teamImpl.addTeam("Utah Jazz", basketball); + teamImpl.addTeam("Denver Nuggets", basketball); + teamImpl.addTeam("Minnesota Timberwolves", basketball); + teamImpl.addTeam("Dallas Mavericks", basketball); + teamImpl.addTeam("Seattle SuperSonics", basketball); + teamImpl.addTeam("Phoenix Suns", basketball); + teamImpl.addTeam("Golden State Warriors", basketball); + teamImpl.addTeam("Portland Trail Blazers", basketball); + teamImpl.addTeam("Los Angeles Lakers", basketball); + teamImpl.addTeam("Sacramento Kings", basketball); + teamImpl.addTeam("Los Angeles Clippers", basketball); + teamImpl.addTeam("New York Knicks", basketball); + teamImpl.addTeam("Orlando Magic", basketball); + teamImpl.addTeam("New Jersey Nets", basketball); + teamImpl.addTeam("Miami Heat", basketball); + teamImpl.addTeam("Boston Celtics", basketball); + teamImpl.addTeam("Philadelphia 76ers", basketball); + teamImpl.addTeam("Washington Bullets", basketball); + teamImpl.addTeam("Atlanta Hawks", basketball); + teamImpl.addTeam("Chicago Bulls", basketball); + teamImpl.addTeam("Indiana Pacers", basketball); + teamImpl.addTeam("Cleveland Cavaliers", basketball); + teamImpl.addTeam("Charlotte Hornets", basketball); + teamImpl.addTeam("Detroit Pistons", basketball); + teamImpl.addTeam("Milwaukee Bucks", basketball); + } + + public void setupHockeyTeams(SportEntity hockey) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("New York Rangers", hockey); + teamImpl.addTeam("Buffalo Sabers", hockey); + teamImpl.addTeam("Detroit Red Wings", hockey); + teamImpl.addTeam("Vancouver Canucks", hockey); + teamImpl.addTeam("Mighty Ducks of Anaheim", hockey); + teamImpl.addTeam("Calgary Flames", hockey); + teamImpl.addTeam("Edmonton Oilers", hockey); + teamImpl.addTeam("Los Angeles Kings", hockey); + teamImpl.addTeam("San Jose Sharks", hockey); + teamImpl.addTeam("Chicago Blackhawks", hockey); + teamImpl.addTeam("Dallas Stars", hockey); + teamImpl.addTeam("St. Louis Blues", hockey); + teamImpl.addTeam("Toronto Maple Leafs", hockey); + teamImpl.addTeam("Winnipeg Jets", hockey); + teamImpl.addTeam("Boston Bruins", hockey); + teamImpl.addTeam("Hartford Whalers", hockey); + teamImpl.addTeam("Montreal Canadiers", hockey); + teamImpl.addTeam("Ottawa Senators", hockey); + teamImpl.addTeam("Pittsburgh Penguins", hockey); + teamImpl.addTeam("Quebec Nordiques", hockey); + teamImpl.addTeam("Florida Panthers", hockey); + teamImpl.addTeam("New Jersey Devils", hockey); + teamImpl.addTeam("New York Islanders", hockey); + teamImpl.addTeam("Philadelphia Flyers", hockey); + teamImpl.addTeam("Tamba Bay Lightning", hockey); + teamImpl.addTeam("Washington Capitals", hockey); + } + + @After + public void tearDown() { + TeamImpl teamImpl = new TeamImpl(); + Collection teamEntities = teamImpl.findAll(); + for (Iterator iterator = teamEntities.iterator(); iterator.hasNext(); ) { + TeamEntity next = iterator.next(); + teamImpl.delete(next); + } + SportImpl sportImpl = new SportImpl(); + Collection sportEntities = sportImpl.findAll(); + for (Iterator iterator = sportEntities.iterator(); iterator.hasNext(); ) { + SportEntity next = iterator.next(); + sportImpl.delete(next); + } + } + + @Test + public void gettAllSports() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + Assert.assertEquals(4, resultList.size()); + } + + @Test + public void getAllTeams() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + Assert.assertEquals(111, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java new file mode 100644 index 0000000..00785cc --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class BaseSetImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBaseSetAddAndDelete() { + + } + + @Test + public void testBaseSetFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java new file mode 100644 index 0000000..c7fe97a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + + @Test + public void testInsertAddAndDelete() { + + } + + @Test + public void testInsertFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..972aa84 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,52 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class ManufacturerImplTest { + + @Before + public void setup() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void addManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + ManufacturerEntity manufacturer = manufacturerImpl.addManufacturer(manufacturerName); + Assert.assertNotNull(manufacturer); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertTrue(manufacturerList.size() > 0); + } + + @Test + public void deleteManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertTrue(manufacturerList.size() > 0); + manufacturerImpl.delete(manufacturerList.get(0)); + manufacturerList = manufacturerImpl.findByName(manufacturerName); + Assert.assertEquals(0, manufacturerList.size()); + } + + @Test + public void testManufacturerAddAndDelete() { + + } + + @Test + public void testManufacturerFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java new file mode 100644 index 0000000..19cc143 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testParallelSetAddAndDelete() { + + } + + @Test + public void testParallelSetFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java new file mode 100644 index 0000000..6aca0db --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPlayerAddAndDelete() { + + } + + @Test + public void testPlayerFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java new file mode 100644 index 0000000..3d2019b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java @@ -0,0 +1,42 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPositionAddAndDelete() { + String positionName = "testPositionAddAndDelete"; + PositionImpl positionImpl = new PositionImpl(); + PositionEntity position = positionImpl.addPosition(positionName); + Collection resultList = positionImpl.findByName(positionName); + Assert.assertNotNull(resultList); + Assert.assertEquals(1, resultList.size()); + positionImpl.delete(position); + resultList = positionImpl.findByName(positionName); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testPositionFindAll() { + PositionImpl positionImpl = new PositionImpl(); + Collection resultList = positionImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} + diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java new file mode 100644 index 0000000..4e9d190 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.Before; +import org.junit.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportCardAddAndDelete() { + + } + + @Test + public void testSportCardFindAll() { + + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java new file mode 100644 index 0000000..f8053c5 --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class SportImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportAddAndDelete() { + String sportName = "testSportAddAndDelete"; + SportImpl sportImpl = new SportImpl(); + SportEntity sport = sportImpl.addSport(sportName); + List sportList = sportImpl.findByName(sportName); + Assert.assertEquals(1, sportList.size()); + sportImpl.delete(sport); + List result = sportImpl.findByName(sportName); + Assert.assertEquals(0, result.size()); + } + + @Test + public void testSportFindAll() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java new file mode 100644 index 0000000..1cfec7b --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collection; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class TeamImplTest { + + @Before + public void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTeamAddAndDelete() { + String teamName = "testTeamAddAndDelete"; + TeamEntity team = new TeamEntity(teamName); + TeamImpl teamImpl = new TeamImpl(); + teamImpl.store(team); + Collection resultList = teamImpl.findByName(teamName); + Assert.assertEquals(1, resultList.size()); + teamImpl.delete(team); + resultList = teamImpl.findByName(teamName); + Assert.assertEquals(0, resultList.size()); + } + + @Test + public void testTeamFindAll() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + Assert.assertEquals(0, resultList.size()); + } +} diff --git a/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java new file mode 100644 index 0000000..503104a --- /dev/null +++ b/java-ee/KontorApp/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java @@ -0,0 +1,110 @@ +package com.ibtp.kontor.util; + +import com.ibtp.kontor.dal.Database; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public class LocalTestDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalTestDatabase.class.getName()); + + static { + logger.info("initialization and starting database"); + LocalTestDatabase.assureDatabaseRunning(); + } + + public LocalTestDatabase() { + logger.info("LocalDatabaseTest started"); + } + + private static void assureDatabaseRunning() { + if (LocalTestDatabase.server == null) { + LocalTestDatabase.startDatabase(); + } + } + + private static void startDatabase() { + logger.info("startDatabase as kontor in hsqldb_databases/test"); + LocalTestDatabase.server = new Server(); + LocalTestDatabase.server.setAddress("localhost"); + LocalTestDatabase.server.setDatabaseName(0, "kontor"); + LocalTestDatabase.server.setDatabasePath(0, "file:build/hsqldb_databases/test"); + LocalTestDatabase.server.setPort(2345); + LocalTestDatabase.server.setTrace(true); + LocalTestDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalTestDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalTestDatabase.factory == null) { + LocalTestDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalTestDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalTestDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalTestDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalTestDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalTestDatabase.server.toString(); + } + return LocalTestDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml b/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java-ee/KontorApp/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java-ee/KontorApp/src/test/resources/logback.xml b/java-ee/KontorApp/src/test/resources/logback.xml new file mode 100644 index 0000000..5254e50 --- /dev/null +++ b/java-ee/KontorApp/src/test/resources/logback.xml @@ -0,0 +1,41 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + build/kontortest.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + build/kontortest.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + + diff --git a/java-ee/KontorEJB/build.gradle b/java-ee/KontorEJB/build.gradle new file mode 100755 index 0000000..5f1dfc0 --- /dev/null +++ b/java-ee/KontorEJB/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'java' + +dependencies { + compile 'javax.enterprise:cdi-api:+' + compile 'org.jboss.spec.javax.faces:jboss-jsf-api_2.2_spec:+' + compile 'org.jboss.spec.javax.ejb:jboss-ejb-api_3.2_spec:+' + compile 'org.jboss.spec.javax.annotation:jboss-annotations-api_1.2_spec:+' + compile 'org.hibernate.ogm:hibernate-ogm-mongodb:+' + compile 'org.eclipse.persistence:javax.persistence:2.1.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' +} diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java new file mode 100755 index 0000000..67b84ce --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Controller.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.ejb; + +import com.ibtp.kontor.ejb.PropertyManager; +import com.ibtp.kontor.ejb.Property; + +import java.util.List; +import javax.annotation.PostConstruct; +import javax.enterprise.inject.Model; + +import javax.inject.Inject; + +@Model +public class Controller { + + List propertyList; + + private String key; + private String value; + + @PostConstruct + public void readDB() { + propertyList = ejb.queryCache(); + + } + @Inject + PropertyManager ejb; + + public void save() { + Property p = new Property(); + p.setKey(key); + p.setValue(value); + ejb.save(p); + propertyList.add(p); + key = ""; + value = ""; + } + + public List getPropertyList() { + return propertyList; + } + + public void setPropertyList(List propertyList) { + this.propertyList = propertyList; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} \ No newline at end of file diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java new file mode 100755 index 0000000..d2fab7b --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/Property.java @@ -0,0 +1,37 @@ +package com.ibtp.kontor.ejb; + + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import org.hibernate.annotations.GenericGenerator; + +@Entity +public class Property { + + @Id + @GeneratedValue(generator = "uuid") + @GenericGenerator(name = "uuid", strategy = "uuid2") + private String id; + + private String key; + + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} \ No newline at end of file diff --git a/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java new file mode 100755 index 0000000..865a0bd --- /dev/null +++ b/java-ee/KontorEJB/src/main/java/com/ibtp/kontor/ejb/PropertyManager.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.ejb; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.ibtp.kontor.ejb.Property; +import javax.ejb.Stateless; + +@Stateless + public class PropertyManager { + + @PersistenceContext(unitName = "mongo-ogm") + private EntityManager em; + + public void save(Property p) { + em.persist(p); + } + + public List queryCache() { + Query query = em.createQuery("FROM Property p"); + + List list = query.getResultList(); + return list; + } +} \ No newline at end of file diff --git a/java-ee/KontorImpl/build.gradle b/java-ee/KontorImpl/build.gradle new file mode 100644 index 0000000..b1512a8 --- /dev/null +++ b/java-ee/KontorImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Kontor', 'Implementation-Version': version + } +} diff --git a/java-ee/KontorImpl/config/checkstyle/checkstyle.xml b/java-ee/KontorImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/KontorImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl b/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/KontorImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/KontorImpl/config/findbugs/findbugs.xml b/java-ee/KontorImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/KontorImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java new file mode 100644 index 0000000..f2c37f6 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserDao.java @@ -0,0 +1,16 @@ +package com.peetz.kontor.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.kontor.entity.KontorUserEntity; + +@Local +public interface KontorUserDao { + public KontorUserEntity getById(Long id); + public List findByIds(List ids); + public KontorUserEntity findByLogin(String login); + public KontorUserEntity store(KontorUserEntity entity); + public void delete(KontorUserEntity entity); +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java new file mode 100644 index 0000000..3dee67f --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/dal/KontorUserImpl.java @@ -0,0 +1,53 @@ +package com.peetz.kontor.dal; + +import java.util.List; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +import com.peetz.kontor.entity.KontorUserEntity; + +@Stateless(name = "ComicDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class KontorUserImpl implements KontorUserDao { + + @PersistenceContext + private EntityManager em; + + @Override + public KontorUserEntity getById(Long id) { + Query q = em.createNamedQuery("findById"); + q.setParameter("id", id); + KontorUserEntity entity = (KontorUserEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + // TODO Auto-generated method stub + return null; + } + + @Override + public KontorUserEntity findByLogin(String login) { + Query q = em.createNamedQuery("findByLogin"); + q.setParameter("login", login); + KontorUserEntity entity = (KontorUserEntity)q.getSingleResult(); + return entity; + } + + @Override + public KontorUserEntity store(KontorUserEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(KontorUserEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java new file mode 100644 index 0000000..5b8a0b5 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/entity/KontorUserEntity.java @@ -0,0 +1,62 @@ +package com.peetz.kontor.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="findAll", query="SELECT u from KontorUser as u"), + @NamedQuery(name="findById", query="SELECT u from KontorUser as u WHERE u.id = :id"), + @NamedQuery(name="findByLogin", query="SELECT u from KontorUser as u WHERE u.login = :login") +}) + +@Entity +@Table(name="KONTORUSER") +public class KontorUserEntity { + private Long id; + + private String login; + + private String password; + + private String name; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + @Column + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java new file mode 100644 index 0000000..e64dc61 --- /dev/null +++ b/java-ee/KontorImpl/src/main/java/com/peetz/kontor/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.kontor.service; \ No newline at end of file diff --git a/java-ee/KontorWeb/build.gradle b/java-ee/KontorWeb/build.gradle new file mode 100755 index 0000000..e6eb2e0 --- /dev/null +++ b/java-ee/KontorWeb/build.gradle @@ -0,0 +1,11 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':KontorEJB') + compile project(':ComicsWeb') + compile project(':MedienWeb') + compile project(':LibraryWeb') + compile project(':TradingCardsWeb') +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java new file mode 100644 index 0000000..180eeda --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportComics.java @@ -0,0 +1,88 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.comics.entity.ArtistEntity; +import com.peetz.comics.entity.ComicEntity; +import com.peetz.comics.entity.IssueEntity; +import com.peetz.comics.entity.PublisherEntity; +import com.peetz.comics.entity.StoryArcEntity; +import com.peetz.comics.service.ComicService; + +public class ExportComics +{ + public Element backupComics(Document document) + { + Element comics = document.createElement("comics"); + ComicService comicService = getComicService(); + if (comicService == null) return comics; + + Collection publishers = comicService.getAllPublisher(); + Iterator publisher_iterator = publishers.iterator(); + while (publisher_iterator.hasNext()) { + PublisherEntity publisher = publisher_iterator.next(); + Element publisherNode = document.createElement("publisher"); + publisherNode.setAttribute("id", publisher.getId().toString()); + publisherNode.setAttribute("name", publisher.getName()); + comics.appendChild(publisherNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection artists = comicService.getAllArtists(); + Iterator artist_iterator = artists.iterator(); + while(artist_iterator.hasNext()) { + ArtistEntity artist = artist_iterator.next(); + Element artistNode = document.createElement("artist"); + artistNode.setAttribute("id", artist.getId().toString()); + artistNode.setAttribute("name", artist.getName()); + comics.appendChild(artistNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection comicList = comicService.getAllComics(); + Iterator comics_iterator = comicList.iterator(); + while(comics_iterator.hasNext()) { + ComicEntity comic = comics_iterator.next(); + Element comicNode = document.createElement("comic"); + comicNode.setAttribute("id", comic.getId().toString()); + comicNode.setAttribute("title", comic.getTitle()); + String completed = "false"; + if (comic.getCompleted() != null) completed = comic.getCompleted().toString(); + comicNode.setAttribute("complete", completed); + String currentOrder = "false"; + if (comic.getCurrentOrder() != null) currentOrder = comic.getCurrentOrder().toString(); + comicNode.setAttribute("order", currentOrder); + Collection issues = comicService.getAllIssuesForComic(comic); + Iterator issues_iterator = issues.iterator(); + comicNode.appendChild(document.createTextNode("\n")); + while(issues_iterator.hasNext()) { + IssueEntity issue = issues_iterator.next(); + Element issueNode = document.createElement("issue"); + issueNode.setAttribute("id", issue.getId().toString()); + issueNode.setAttribute("number", issue.getNumber()); + comicNode.appendChild(issueNode); + comicNode.appendChild(document.createTextNode("\n")); + } + comics.appendChild(comicNode); + comics.appendChild(document.createTextNode("\n")); + } + Collection storyArcs = comicService.getAllStoryArcs(); + Iterator iterator_storyArcsIterator = storyArcs.iterator(); + while(iterator_storyArcsIterator.hasNext()) { + StoryArcEntity storyArc = iterator_storyArcsIterator.next(); + Element storyNode = document.createElement("storyArc"); + storyNode.setAttribute("id", storyArc.getId().toString()); + storyNode.setAttribute("title", storyArc.getTitle()); + comics.appendChild(storyNode); + comics.appendChild(document.createTextNode("\n")); + } + return comics; + } + + private ComicService getComicService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java new file mode 100644 index 0000000..1c17da9 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportLibrary.java @@ -0,0 +1,100 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; +import com.peetz.library.entity.ShelfboardEntity; +import com.peetz.library.service.LibraryService; + +public class ExportLibrary +{ + public Element backupLibrary(Document document) + { + Element library = document.createElement("library"); + library.appendChild(document.createTextNode("\n")); + LibraryService libraryService = getLibraryService(); + + Collection books = libraryService.getAllBooks(); + Iterator iterator_books = books.iterator(); + while(iterator_books.hasNext()) { + BookEntity book = iterator_books.next(); + Element bookNode = document.createElement("book"); + bookNode.setAttribute("id", book.getId().toString()); + bookNode.setAttribute("title", book.getTitle()); + bookNode.setAttribute("author", book.getAuthor()); + bookNode.setAttribute("edition", book.getEdition()); + bookNode.setAttribute("isbn", book.getIsbn()); + bookNode.setAttribute("pages", book.getPage().toString()); + bookNode.setAttribute("publisher", book.getPublisher()); + library.appendChild(bookNode); + library.appendChild(document.createTextNode("\n")); + } + + Collection bookshelfs = libraryService.getAllBookshelfs(); + Iterator iterator_bookshelfs = bookshelfs.iterator(); + while(iterator_bookshelfs.hasNext()) { + BookshelfEntity bookshelf = iterator_bookshelfs.next(); + Element shelfNode = document.createElement("shelf"); + shelfNode.setAttribute("id", bookshelf.getId().toString()); + shelfNode.setAttribute("title", bookshelf.getTitle()); + + shelfNode.appendChild(document.createTextNode("\n")); + Collection shelfboards = bookshelf.getShelfBoards(); + Iterator iterator_shelfboards = shelfboards.iterator(); + while(iterator_shelfboards.hasNext()) { + ShelfboardEntity shelfboard = iterator_shelfboards.next(); + Element boardNode = document.createElement("board"); + boardNode.setAttribute("id", shelfboard.getId().toString()); + boardNode.setAttribute("title", shelfboard.getTitle()); + shelfNode.appendChild(boardNode); + shelfNode.appendChild(document.createTextNode("\n")); + } + library.appendChild(shelfNode); + library.appendChild(document.createTextNode("\n")); + } + + Collection articles = libraryService.getAllArticles(); + Iterator iterator_articles = articles.iterator(); + while(iterator_articles.hasNext()) { + ArticleEntity article = iterator_articles.next(); + Element articleNode = document.createElement("article"); + articleNode.setAttribute("id", article.getId().toString()); + articleNode.setAttribute("title", article.getTitle()); + + Collection origins = article.getOriginArticles(); + Iterator iterator_origins = origins.iterator(); + while(iterator_origins.hasNext()) { + ArticleEntity origin = iterator_origins.next(); + Element originNode = document.createElement("article"); + originNode.setAttribute("id", origin.getId().toString()); + articleNode.appendChild(originNode); + articleNode.appendChild(document.createTextNode("\n")); + } + + Collection relateds = article.getRelatedArticles(); + Iterator iterator_relateds = relateds.iterator(); + articleNode.appendChild(document.createTextNode("\n")); + while(iterator_relateds.hasNext()) { + ArticleEntity related = iterator_relateds.next(); + Element relatedNode = document.createElement("article"); + relatedNode.setAttribute("id", related.getId().toString()); + articleNode.appendChild(relatedNode); + articleNode.appendChild(document.createTextNode("\n")); + } + library.appendChild(articleNode); + library.appendChild(document.createTextNode("\n")); + } + return library; + } + + private LibraryService getLibraryService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java new file mode 100644 index 0000000..48d0ee4 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportMedien.java @@ -0,0 +1,75 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; +import com.peetz.medien.service.MedienService; + +public class ExportMedien +{ + public Element backupMedien(Document document) + { + Element medien = document.createElement("medien"); + medien.appendChild(document.createTextNode("\n")); + + MedienService medienService = getMedienService(); + + Collection cds = medienService.getAllCDs(); + Iterator iterator_cds = cds.iterator(); + while(iterator_cds.hasNext()) { + AudioCDEntity cd = iterator_cds.next(); + Element cdNode = document.createElement("audioCD"); + //cdNode.setAttribute("id", cd.getId()); + //cdNode.setAttribute("album", cd.getAlbum()); + //cdNode.setAttribute("artist", cd.getArtist()); + medien.appendChild(cdNode); + medien.appendChild(document.createTextNode("\n")); + } + + Collection films = medienService.getAllDVDs(); + Iterator iterator_films = films.iterator(); + while(iterator_films.hasNext()) { + FilmEntity film = iterator_films.next(); + Element filmNode = document.createElement("film"); + //filmNode.setAttribute("id", film.getId()); + //filmNode.setAttribute("title", film.getTitle()); + medien.appendChild(filmNode); + medien.appendChild(document.createTextNode("\n")); + } + + Collection boxsets = medienService.getAllBoxSets(); + Iterator iterator_boxsets = boxsets.iterator(); + while(iterator_boxsets.hasNext()) { + BoxSetEntity boxSet = iterator_boxsets.next(); + Element boxNode = document.createElement("boxSet"); + boxNode.setAttribute("id", boxSet.getId().toString()); + boxNode.setAttribute("title", boxSet.getTitle()); + films = boxSet.getFilms(); + iterator_films = films.iterator(); + if (iterator_films.hasNext()) { + boxNode.appendChild(document.createTextNode("\n")); + } + while(iterator_films.hasNext()) { + FilmEntity film = iterator_films.next(); + Element filmNode = document.createElement("film"); + //filmNode.setAttribute("id", film.getId()); + boxNode.appendChild(filmNode); + boxNode.appendChild(document.createTextNode("\n")); + } + medien.appendChild(boxNode); + medien.appendChild(document.createTextNode("\n")); + } + return medien; + } + + private MedienService getMedienService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java new file mode 100644 index 0000000..f17e424 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ExportTradingCards.java @@ -0,0 +1,175 @@ +package com.peetz.kontor.data; + +import java.util.Collection; +import java.util.Iterator; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportCardEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; +import com.peetz.tradingcards.service.SportService; +import com.peetz.tradingcards.service.TradingcardService; + +public class ExportTradingCards +{ + public Element backupTradingCards(Document document) + { + Element tradingcards = document.createElement("tradingcards"); + tradingcards.appendChild(document.createTextNode("\n")); + + SportService sportService = getSportService(); + backupSports(sportService, document, tradingcards); + + backupCards(document, tradingcards); + + return tradingcards; + } + + private void backupSports(SportService sportService, Document document, Element tradingcards) + { + Collection sports = sportService.getAllSports(); + Iterator iterator_sports = sports.iterator(); + while(iterator_sports.hasNext()) { + SportEntity sport = iterator_sports.next(); + Element sportNode = document.createElement("sport"); + sportNode.setAttribute("id", sport.getId().toString()); + sportNode.setAttribute("name", sport.getName()); + + Collection teams = sportService.getTeams(sport); + backupTeams(teams, document, sportNode); + + Collection positions = sportService.getPositions(sport); + backupPositions(positions, document, sportNode); + + tradingcards.appendChild(sportNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + } + + private void backupTeams(Collection teams, Document document, Element sportNode) + { + Iterator iterator_teams = teams.iterator(); + while(iterator_teams.hasNext()) { + TeamEntity team = iterator_teams.next(); + Element teamNode = document.createElement("team"); + teamNode.setAttribute("id", team.getId().toString()); + teamNode.setAttribute("name", team.getName()); + //TODO what happens with null attributes? + //teamNode.setAttribute("short", teamView.getShortname()); + sportNode.appendChild(teamNode); + } + } + + private void backupPositions(Collection positions, Document document, Element sportNode) + { + Iterator iterator_positions = positions.iterator(); + while(iterator_positions.hasNext()) { + PositionEntity position = iterator_positions.next(); + Element positionNode = document.createElement("position"); + positionNode.setAttribute("id", position.getId().toString()); + positionNode.setAttribute("name", position.getName()); + positionNode.setAttribute("short", position.getShortName()); + sportNode.appendChild(positionNode); + } + sportNode.appendChild(document.createTextNode("\n")); + } + + private void backupCards(Document document, Element tradingcards) + { + Collection manufacturers = getTradingcardService().getAllManufacturers(); + Iterator iterator_manufacturers = manufacturers.iterator(); + while(iterator_manufacturers.hasNext()) { + ManufacturerEntity manufacturer = iterator_manufacturers.next(); + Element manufacturerNode = document.createElement("manufacturer"); + manufacturerNode.setAttribute("id", manufacturer.getId().toString()); + manufacturerNode.setAttribute("name", manufacturer.getName()); + Collection baseSets = getTradingcardService().getBaseSetsByManufacturer(manufacturer); + backupBaseSets(baseSets, document, manufacturerNode); + Collection parallelSets = getTradingcardService().getParallelSetsByManufacturer(manufacturer); + backupParallelSets(parallelSets, document, manufacturerNode); + Collection inserts = getTradingcardService().getInsertsByManufacturer(manufacturer); + backupInserts(inserts, document, manufacturerNode); + manufacturerNode.appendChild(document.createTextNode("\n")); + tradingcards.appendChild(manufacturerNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + + Collection sportCards = getTradingcardService().getAllSportCards(); + Iterator iterator_sportCards = sportCards.iterator(); + while(iterator_sportCards.hasNext()) { + SportCardEntity sportCard = iterator_sportCards.next(); + Element cardNode = document.createElement("sportCard"); + cardNode.setAttribute("id", sportCard.getId().toString()); + cardNode.setAttribute("player", sportCard.getPlayer().getId().toString()); + cardNode.setAttribute("baseSet", sportCard.getBaseSet().getId().toString()); + if (sportCard.getParallelSet().getId() != null) + { + cardNode.setAttribute("parallelSet", sportCard.getParallelSet().getId().toString()); + } + if (sportCard.getInsert().getId() != null) + { + cardNode.setAttribute("insert", sportCard.getInsert().getId().toString()); + } + tradingcards.appendChild(cardNode); + tradingcards.appendChild(document.createTextNode("\n")); + } + } + + private void backupBaseSets(Collection baseSets, Document document, Element manufacturerNode) + { + Iterator iterator_baseSets= baseSets.iterator(); + while(iterator_baseSets.hasNext()) { + BaseSetEntity baseSet = iterator_baseSets.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element baseSetNode = document.createElement("baseSet"); + baseSetNode.setAttribute("id", baseSet.getId().toString()); + baseSetNode.setAttribute("name", baseSet.getName()); + manufacturerNode.appendChild(baseSetNode); + } + } + + private void backupParallelSets(Collection parallelSets, Document document, Element manufacturerNode) + { + Iterator iterator_parallelSets = parallelSets.iterator(); + while(iterator_parallelSets.hasNext()) { + ParallelSetEntity parallelSet = iterator_parallelSets.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element parallelSetNode = document.createElement("parallelSet"); + parallelSetNode.setAttribute("id", parallelSet.getId().toString()); + parallelSetNode.setAttribute("name", parallelSet.getName()); + parallelSetNode.setAttribute("baseSet", parallelSet.getBaseSet().getId().toString()); + manufacturerNode.appendChild(parallelSetNode); + } + } + + private void backupInserts(Collection inserts, Document document, Element manufacturerNode) + { + Iterator iterator_inserts = inserts.iterator(); + while(iterator_inserts.hasNext()) { + InsertEntity insert = iterator_inserts.next(); + manufacturerNode.appendChild(document.createTextNode("\n")); + Element insertNode = document.createElement("insert"); + insertNode.setAttribute("id", insert.getId().toString()); + insertNode.setAttribute("name", insert.getName()); + insertNode.setAttribute("baseSet", insert.getBaseSet().getId().toString()); + manufacturerNode.appendChild(insertNode); + } + } + + private SportService getSportService() + { + return null; + } + + private TradingcardService getTradingcardService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java new file mode 100644 index 0000000..fb1848f --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileExport.java @@ -0,0 +1,64 @@ +package com.peetz.kontor.data; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class FileExport +{ + public void exportFile(java.io.PrintWriter out) + { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + ExportComics exportComics = new ExportComics(); + ExportMedien exportMedien = new ExportMedien(); + ExportLibrary exportLibrary = new ExportLibrary(); + ExportTradingCards exportTradingCards = new ExportTradingCards(); + + Element root = document.createElement("kontor"); + document.appendChild(root); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportComics.backupComics(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportMedien.backupMedien(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportLibrary.backupLibrary(document)); + root.appendChild(document.createTextNode("\n")); + root.appendChild(exportTradingCards.backupTradingCards(document)); + root.appendChild(document.createTextNode("\n")); + + TransformerFactory tFactory = TransformerFactory.newInstance(); + Transformer transformer = tFactory.newTransformer(); + + DOMSource source = new DOMSource(document); + StreamResult result = new StreamResult(out); + transformer.transform(source, result); + } catch (DOMException e) { + System.out.println(e.getMessage()); + } catch (TransformerConfigurationException e) { + System.out.println(e.getMessage()); + } catch (FactoryConfigurationError e) { + System.out.println(e.getMessage()); + } catch (ParserConfigurationException e) { + System.out.println(e.getMessage()); + } catch (TransformerFactoryConfigurationError e) { + System.out.println(e.getMessage()); + } catch (TransformerException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java new file mode 100644 index 0000000..7b03dee --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/FileImport.java @@ -0,0 +1,72 @@ +package com.peetz.kontor.data; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +public class FileImport +{ + public void importFile(File file) + { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(file); + + Node xmlNode = document.getFirstChild(); + if (!xmlNode.getNodeName().equals("kontor")) return; + NodeList nodeList = xmlNode.getChildNodes(); + for (int i=0; i songs = new ArrayList(); + + System.out.println("AudioCD: " + id + ", " + album + "/" + artist); + AudioCDEntity audioCD = getMedienService().addCD(album); + audioCD.setArtist(artist); + audioCD.setCategory(category); + audioCD.setReleaseYear(year); + audioCD.setWantList(wantList); + audioCD.getSongs().addAll(songs); + getMedienService().saveCD(audioCD); + } + + public void parseFilm(Node node) + { + NamedNodeMap attr = node.getAttributes(); + Node idNode = attr.getNamedItem("id"); + String id = idNode.getNodeValue(); + + Node titleNode = attr.getNamedItem("title"); + String title = titleNode.getNodeValue(); + + System.out.println("DVD: " + id + ", " + title); + MedienService service = getMedienService(); + + service.addDVD(title); + } + + public void parseBoxSet(Node node) + { + NamedNodeMap attr = node.getAttributes(); + Node idNode = attr.getNamedItem("id"); + String id = idNode.getNodeValue(); + + Node titleNode = attr.getNamedItem("title"); + String title = titleNode.getNodeValue(); + + System.out.println("BoxSet: " + id + ", " + title); + MedienService service = getMedienService(); + + service.addBoxSet(title); + } + + private MedienService getMedienService() + { + return null; + } +} diff --git a/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java new file mode 100644 index 0000000..79c4e23 --- /dev/null +++ b/java-ee/KontorWeb/src/main/java/com/peetz/kontor/data/ImportTradingCards.java @@ -0,0 +1,52 @@ +package com.peetz.kontor.data; + +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.peetz.tradingcards.service.SportService; +import com.peetz.tradingcards.service.TradingcardService; + +public class ImportTradingCards +{ + public void parseNode(Node xmlNode) + { + NodeList childNodes = xmlNode.getChildNodes(); + for (int i=0; i + + + + org.eclipse.persistence.jpa.PersistenceProvider + jdbc/kontor + + + com.peetz.kontor.entity.KontorUserEntity + + com.peetz.comics.entity.ArtistEntity + com.peetz.comics.entity.ComicEntity + com.peetz.comics.entity.IssueEntity + com.peetz.comics.entity.PublisherEntity + com.peetz.comics.entity.StoryArcEntity + com.peetz.comics.entity.VolumeEntity + + com.peetz.library.entity.ArticleEntity + com.peetz.library.entity.BookEntity + com.peetz.library.entity.BookshelfEntity + com.peetz.library.entity.FileEntity + com.peetz.library.entity.MagazineEntity + com.peetz.library.entity.ShelfObjectEntity + com.peetz.library.entity.ShelfboardEntity + + com.peetz.medien.entity.AudioCDEntity + com.peetz.medien.entity.BoxSetEntity + com.peetz.medien.entity.FilmEntity + + com.peetz.tradingcards.entity.BaseSetEntity + com.peetz.tradingcards.entity.InsertEntity + com.peetz.tradingcards.entity.ManufacturerEntity + com.peetz.tradingcards.entity.ParallelSetEntity + com.peetz.tradingcards.entity.PlayerEntity + com.peetz.tradingcards.entity.PositionEntity + com.peetz.tradingcards.entity.SportCardEntity + com.peetz.tradingcards.entity.SportEntity + com.peetz.tradingcards.entity.TeamEntity + + + + + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml new file mode 100644 index 0000000..77df4e5 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/faces-config.xml @@ -0,0 +1,77 @@ + + + + + kontor + /index.xhtml + + + comics + /comics.xhtml + + + library + /library.xhtml + + + medien + /medien.xhtml + + + tradingcards + /tradingcards.xhtml + + + sport + /sport.xhtml + + + sportAdd + /sport/sportAdd.xhtml + + + + /sport/sportAdd.xhtml + + addSport + /sport/sportDetails.xhtml + + + saveSport + /sport/sportDetails.xhtml + + + + now + java.util.Date + request + + + comicView + com.peetz.comics.view.ComicView + request + + + libraryView + com.peetz.library.view.LibraryView + request + + + medienView + com.peetz.medien.view.MedienView + request + + + tradingCardsView + com.peetz.tradingcards.view.TradingCardsView + request + + + sportView + com.peetz.tradingcards.view.SportView + request + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml new file mode 100644 index 0000000..c95df12 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/glassfish-web.xml @@ -0,0 +1,7 @@ + + + + /kontor + + + diff --git a/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml b/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..112c02f --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + javax.faces.PROJECT_STAGE + Development + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + Faces Servlet + /faces/* + + + 30 + + + faces/index.xhtml + + diff --git a/java-ee/KontorWeb/src/main/webapp/comics.xhtml b/java-ee/KontorWeb/src/main/webapp/comics.xhtml new file mode 100644 index 0000000..15d340f --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/comics.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Comics Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/css/store.css b/java-ee/KontorWeb/src/main/webapp/css/store.css new file mode 100755 index 0000000..de05f30 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/css/store.css @@ -0,0 +1,145 @@ +.spring { + border: thin solid black; + + font-size:10px; + font-family:Arial; + font-weight:normal; + background-color: #ABE7FA; + + } +.myButton { + -moz-box-shadow: 0px 10px 14px -7px #276873; + -webkit-box-shadow: 0px 10px 14px -7px #276873; + box-shadow: 0px 10px 14px -7px #276873; + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #599bb3), color-stop(1, #408c99)); + background:-moz-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-webkit-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-o-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:-ms-linear-gradient(top, #599bb3 5%, #408c99 100%); + background:linear-gradient(to bottom, #599bb3 5%, #408c99 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#599bb3', endColorstr='#408c99',GradientType=0); + background-color:#599bb3; + -moz-border-radius:8px; + -webkit-border-radius:8px; + border-radius:8px; + display:inline-block; + cursor:pointer; + color:#ffffff; + font-family:Arial; + font-size:12px; + font-weight:bold; + padding:7px 15px; + text-decoration:none; + text-shadow:0px 1px 0px #3d768a; +} +.myButton:hover { + background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #408c99), color-stop(1, #599bb3)); + background:-moz-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-webkit-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-o-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:-ms-linear-gradient(top, #408c99 5%, #599bb3 100%); + background:linear-gradient(to bottom, #408c99 5%, #599bb3 100%); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#408c99', endColorstr='#599bb3',GradientType=0); + background-color:#408c99; +} +.myButton:active { + position:relative; + top:1px; +} + + + .tablestyle { + margin:0px;padding:0px; + width:50%; + box-shadow: 10px 10px 5px #888888; + border:1px solid #000000; + + -moz-border-radius-bottomleft:0px; + -webkit-border-bottom-left-radius:0px; + border-bottom-left-radius:0px; + + -moz-border-radius-bottomright:0px; + -webkit-border-bottom-right-radius:0px; + border-bottom-right-radius:0px; + + -moz-border-radius-topright:0px; + -webkit-border-top-right-radius:0px; + border-top-right-radius:0px; + + -moz-border-radius-topleft:0px; + -webkit-border-top-left-radius:0px; + border-top-left-radius:0px; +}.tablestyle table{ + border-collapse: collapse; + border-spacing: 0; + width:100%; + height:100%; + margin:0px;padding:0px; +}.tablestyle tr:last-child td:last-child { + -moz-border-radius-bottomright:0px; + -webkit-border-bottom-right-radius:0px; + border-bottom-right-radius:0px; +} +.tablestyle table tr:first-child td:first-child { + -moz-border-radius-topleft:0px; + -webkit-border-top-left-radius:0px; + border-top-left-radius:0px; +} +.tablestyle table tr:first-child td:last-child { + -moz-border-radius-topright:0px; + -webkit-border-top-right-radius:0px; + border-top-right-radius:0px; +}.tablestyle tr:last-child td:first-child{ + -moz-border-radius-bottomleft:0px; + -webkit-border-bottom-left-radius:0px; + border-bottom-left-radius:0px; +}.tablestyle tr:hover td{ + +} +.tablestyle tr:nth-child(odd){ background-color:#aad4ff; } +.tablestyle tr:nth-child(even) { background-color:#ffffff; }.tablestyle td{ + vertical-align:middle; + + + border:1px solid #000000; + border-width:0px 1px 1px 0px; + text-align:left; + padding:7px; + font-size:10px; + font-family:Arial; + font-weight:normal; + color:#000000; +}.tablestyle tr:last-child td{ + border-width:0px 1px 0px 0px; +}.tablestyle tr td:last-child{ + border-width:0px 0px 1px 0px; +}.tablestyle tr:last-child td:last-child{ + border-width:0px 0px 0px 0px; +} +.tablestyle tr:first-child td{ + background:-o-linear-gradient(bottom, #005fbf 5%, #003f7f 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #005fbf), color-stop(1, #003f7f) ); + background:-moz-linear-gradient( center top, #005fbf 5%, #003f7f 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#005fbf", endColorstr="#003f7f"); background: -o-linear-gradient(top,#005fbf,003f7f); + + background-color:#005fbf; + border:0px solid #000000; + text-align:center; + border-width:0px 0px 1px 1px; + font-size:14px; + font-family:Arial; + font-weight:bold; + color:#ffffff; +} +.tablestyle tr:first-child:hover td{ + background:-o-linear-gradient(bottom, #005fbf 5%, #003f7f 100%); background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #005fbf), color-stop(1, #003f7f) ); + background:-moz-linear-gradient( center top, #005fbf 5%, #003f7f 100% ); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#005fbf", endColorstr="#003f7f"); background: -o-linear-gradient(top,#005fbf,003f7f); + + background-color:#005fbf; +} +.tablestyle tr:first-child td:first-child{ + border-width:0px 0px 1px 0px; +} +.tablestyle tr:first-child td:last-child{ + border-width:0px 0px 1px 1px; +} diff --git a/java-ee/KontorWeb/src/main/webapp/index.xhtml b/java-ee/KontorWeb/src/main/webapp/index.xhtml new file mode 100755 index 0000000..870d904 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/index.xhtml @@ -0,0 +1,48 @@ + + + + + + + + + + +
+ + + + + + + + + + + + +
+ + +
+ + + + Key + + + + Value + + + +
+
+ diff --git a/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml b/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml new file mode 100644 index 0000000..90a8e65 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/kontorTemplate.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + Kontor Application + + + + +
+ Top +
+
+
+ + Kontor

+ Comics

+ Library

+ Medien + Trading Cards

+
+
+
+ +
+ Content +
+
+
+
+ Ingenieurbüro Thomas Peetz +
+ +
+ + diff --git a/java-ee/KontorWeb/src/main/webapp/library.xhtml b/java-ee/KontorWeb/src/main/webapp/library.xhtml new file mode 100644 index 0000000..ffeac85 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/library.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Library Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/medien.xhtml b/java-ee/KontorWeb/src/main/webapp/medien.xhtml new file mode 100644 index 0000000..b77bd49 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/medien.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Medien Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css b/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css new file mode 100644 index 0000000..b2f98ff --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/resources/css/cssLayout.css @@ -0,0 +1,71 @@ + +#top { + position: relative; + background-color: lightgrey; + text-align: center; + width: 100%; + height: 30px; + color: white; + padding: 5px; + //margin: 0px 0px 10px 0px; +} + +#bottom { + position: relative; + background-color: lightgrey; + width: 100%; + height: 30px; + padding: 5px; + //margin: 10px 0px 0px 0px; +} + +#left { + float: left; + background-color: tan; + padding: 5px; + width: 150px; + height: 50%; +} + +#right { + float: right; + background-color: tan; + //padding: 5px; + width: 150px; + height: 50%; +} + +.center_content { + position: relative; + background-color: wheat; + padding: 5px; + height: 50%; +} + +.left_content { + background-color: tan; + padding: 5px; + margin-left: 170px; + height: 50%; +} + +.right_content { + background-color: wheat; + padding: 5px; + //margin: 0px 170px 0px 170px; + height: 50%; +} + +#top a:link, #top a:visited { + color: white; + font-weight : bold; + text-decoration: none; +} + +#top a:link:hover, #top a:visited:hover { + color: black; + font-weight : bold; + text-decoration : underline; +} + + diff --git a/java-ee/KontorWeb/src/main/webapp/resources/css/default.css b/java-ee/KontorWeb/src/main/webapp/resources/css/default.css new file mode 100644 index 0000000..6cbc3d1 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/resources/css/default.css @@ -0,0 +1,29 @@ +body { + background-color: #ffffff; + font-size: 12px; + font-family: Verdana, "Verdana CE", Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif; + color: #000000; + margin: 10px; +} + +h1 { + font-family: Arial, "Arial CE", "Lucida Grande CE", lucida, "Helvetica CE", sans-serif; + border-bottom: 1px solid #AFAFAF; + font-size: 16px; + font-weight: bold; + margin: 0px; + padding: 0px; + color: #D20005; +} + +a:link, a:visited { + color: #045491; + font-weight : bold; + text-decoration: none; +} + +a:link:hover, a:visited:hover { + color: #045491; + font-weight : bold; + text-decoration : underline; +} diff --git a/java-ee/KontorWeb/src/main/webapp/seite.html b/java-ee/KontorWeb/src/main/webapp/seite.html new file mode 100644 index 0000000..8478fea --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/seite.html @@ -0,0 +1,17 @@ + + + + Testseite + + +
+
Testseite
+
+ +
Hauptseite
+
Details
+
+ +
+ + \ No newline at end of file diff --git a/java-ee/KontorWeb/src/main/webapp/sport.xhtml b/java-ee/KontorWeb/src/main/webapp/sport.xhtml new file mode 100644 index 0000000..840a202 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport.xhtml @@ -0,0 +1,40 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml b/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml new file mode 100644 index 0000000..ee5ee2d --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport/sportAdd.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml b/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml new file mode 100644 index 0000000..cc6be43 --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/sport/sportDetails.xhtml @@ -0,0 +1,39 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml b/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml new file mode 100644 index 0000000..585697e --- /dev/null +++ b/java-ee/KontorWeb/src/main/webapp/tradingcards.xhtml @@ -0,0 +1,40 @@ + + + + + + + + Kontor Application + + +
Trading Cards Application
+
+
+ Kontor

+ Comics

+ Library

+ Medien

+ Trading Cards

+ Sport

+
+
+ +
+ + + + + + + + +
+
+
+
Ingenieurbüro Thomas Peetz
+ +
+ diff --git a/java-ee/LibraryImpl/build.gradle b/java-ee/LibraryImpl/build.gradle new file mode 100644 index 0000000..fa86a7f --- /dev/null +++ b/java-ee/LibraryImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Library', 'Implementation-Version': version + } +} diff --git a/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/LibraryImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/LibraryImpl/config/findbugs/findbugs.xml b/java-ee/LibraryImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/LibraryImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java new file mode 100644 index 0000000..a62a119 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ArticleDao.java @@ -0,0 +1,25 @@ +package com.peetz.library.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.library.entity.ArticleEntity; + +@Local +public interface ArticleDao { + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public List getRelatedArticles(ArticleEntity entity); + + public ArticleEntity assign(ArticleEntity origin, ArticleEntity reference); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java new file mode 100644 index 0000000..56f2fea --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.BookEntity; + +@Local +public interface BookDao { + public BookEntity findById(Long id); + public Collection findByIds(Collection ids); + public BookEntity store(BookEntity entity); + public void delete(BookEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java new file mode 100644 index 0000000..ddf5b10 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/BookshelfDao.java @@ -0,0 +1,22 @@ +package com.peetz.library.dal; + +import java.util.List; + +import javax.ejb.Local; + +import com.peetz.library.entity.BookshelfEntity; + + +@Local +public interface BookshelfDao { + public BookshelfEntity getById(Long id); + + public List findByIds(List ids); + + public List findByTitle(String title); + + public BookshelfEntity store(BookshelfEntity entity); + + public void delete(BookshelfEntity entity); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java new file mode 100644 index 0000000..b9e2e31 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/FileDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.FileEntity; + +@Local +public interface FileDao { + public FileEntity findById(Long id); + public Collection findByIds(Collection ids); + public FileEntity store(FileEntity entity); + public void delete(FileEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java new file mode 100644 index 0000000..2df143e --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/MagazineDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.MagazineEntity; + +@Local +public interface MagazineDao { + public MagazineEntity findById(Long id); + public Collection findByIds(Collection ids); + public MagazineEntity store(MagazineEntity entity); + public void delete(MagazineEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java new file mode 100644 index 0000000..2028863 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfObjectDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ShelfObjectEntity; + +@Local +public interface ShelfObjectDao { + public ShelfObjectEntity getById(Long id); + public Collection getByIds(Collection ids); + public ShelfObjectEntity store(ShelfObjectEntity entity); + public void delete(ShelfObjectEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java new file mode 100644 index 0000000..ce326a7 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/ShelfboardDao.java @@ -0,0 +1,15 @@ +package com.peetz.library.dal; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ShelfboardEntity; + +@Local +public interface ShelfboardDao { + public ShelfboardEntity getById(Long id); + public Collection getByIds(Collection ids); + public ShelfboardEntity store(ShelfboardEntity entity); + public void delete(ShelfboardEntity entity); +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java new file mode 100644 index 0000000..2c24b31 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/dal/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.dal; \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java new file mode 100644 index 0000000..0348e2b --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ArticleEntity.java @@ -0,0 +1,87 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a") +}) + +@Entity +@Table(name="ARTICLE") +public class ArticleEntity { + private Long id; + + private String title; + + private Collection relatedArticles = new ArrayList(); + + private Collection originArticles = new ArrayList(); + + private FileEntity file; + + private MagazineEntity magazine; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @ManyToMany + public Collection getRelatedArticles() { + return relatedArticles; + } + + public void setRelatedArticles(Collection relatedArticles) { + this.relatedArticles = relatedArticles; + } + + @ManyToMany + public Collection getOriginArticles() { + return originArticles; + } + + public void setOriginArticles(Collection originArticles) { + this.originArticles = originArticles; + } + + @ManyToOne + public FileEntity getFile() { + return file; + } + + public void setFile(FileEntity file) { + this.file = file; + } + + @ManyToOne + public MagazineEntity getMagazine() { + return magazine; + } + + public void setMagazine(MagazineEntity magazine) { + this.magazine = magazine; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java new file mode 100644 index 0000000..1f86ba0 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookEntity.java @@ -0,0 +1,93 @@ +package com.peetz.library.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Book.findAll", query="SELECT a from BookEntity as a") +}) + +@Entity +@Table(name="BOOK") +public class BookEntity { + private Long id; + + private String title; + + private String author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @Column + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java new file mode 100644 index 0000000..521a9bf --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/BookshelfEntity.java @@ -0,0 +1,53 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Bookshelf.findAll", query="SELECT b from BookshelfEntity as b"), + @NamedQuery(name="Bookshelf.findById", query="SELECT b from BookshelfEntity as b WHERE b.id = :id"), + @NamedQuery(name="Bookshelf.findByTitle", query="SELECT b from BookshelfEntity as b WHERE b.title = :title") +}) + +@Entity +@Table(name="BOOKSHELF") +public class BookshelfEntity { + + private Long id; + + private String title; + + private Collection shelfBoards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @OneToMany(mappedBy="bookshelf", cascade=CascadeType.REMOVE) + public Collection getShelfBoards() { + return shelfBoards; + } + + public void setShelfBoards(Collection shelfBoards) { + this.shelfBoards = shelfBoards; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java new file mode 100644 index 0000000..6f31553 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/FileEntity.java @@ -0,0 +1,49 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="FILE") +public class FileEntity { + private Long id; + + private String title; + + private Collection articles = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @OneToMany(mappedBy="file", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java new file mode 100644 index 0000000..6bed864 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/MagazineEntity.java @@ -0,0 +1,49 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="MAGAZINE") +public class MagazineEntity { + private Long id; + + private String title; + + private Collection articles = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @OneToMany(mappedBy="magazine", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java new file mode 100644 index 0000000..98a5cd1 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfObjectEntity.java @@ -0,0 +1,32 @@ +package com.peetz.library.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="SHELFOBJECT") +public class ShelfObjectEntity { + private Long id; + + private ShelfboardEntity shelfboard; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public ShelfboardEntity getShelfboard() { + return shelfboard; + } + + public void setShelfboard(ShelfboardEntity shelfboard) { + this.shelfboard = shelfboard; + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java new file mode 100644 index 0000000..d6e4edd --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/ShelfboardEntity.java @@ -0,0 +1,57 @@ +package com.peetz.library.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="SHELFBOARD") +public class ShelfboardEntity { + private Long id; + + private String title; + + private BookshelfEntity bookshelf; + + private Collection objects = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public BookshelfEntity getBookshelf() { + return bookshelf; + } + + public void setBookshelf(BookshelfEntity bookshelf) { + this.bookshelf = bookshelf; + } + + @OneToMany(mappedBy="shelfboard", cascade=CascadeType.REMOVE) + public Collection getObjects() { + return objects; + } + + public void setObjects(Collection objects) { + this.objects = objects; + } + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java new file mode 100644 index 0000000..94536f3 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.entity; \ No newline at end of file diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java new file mode 100644 index 0000000..7ac294f --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryService.java @@ -0,0 +1,28 @@ +package com.peetz.library.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; + +@Local +public interface LibraryService { + + Collection getAllBooks(); + + Collection getAllBookshelfs(); + + Collection getAllArticles(); + + void addBookshelf(String title); + + void addArticle(String title); + + BookEntity addBook(String title); + + void saveBook(BookEntity book); + +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java new file mode 100644 index 0000000..62baae8 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/LibraryServiceImpl.java @@ -0,0 +1,64 @@ +package com.peetz.library.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.library.entity.ArticleEntity; +import com.peetz.library.entity.BookEntity; +import com.peetz.library.entity.BookshelfEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="LibraryService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class LibraryServiceImpl implements LibraryService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllBooks() { + Query query = em.createNamedQuery("Book.findAll"); + ArrayList bookList = new ArrayList(query.getResultList()); + return bookList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllBookshelfs() { + Query query = em.createNamedQuery("Bookshelf.findAll"); + ArrayList bookshelfList = new ArrayList(query.getResultList()); + return bookshelfList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllArticles() { + Query query = em.createNamedQuery("Article.findAll"); + ArrayList articleList = new ArrayList(query.getResultList()); + return articleList; + } + + @Override + public void addBookshelf(String title) { + } + + @Override + public void addArticle(String title) { + } + + @Override + public BookEntity addBook(String title) { + return null; + } + + @Override + public void saveBook(BookEntity book) { + } +} diff --git a/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java new file mode 100644 index 0000000..4a56b54 --- /dev/null +++ b/java-ee/LibraryImpl/src/main/java/com/peetz/library/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.library.service; \ No newline at end of file diff --git a/java-ee/LibraryWeb/build.gradle b/java-ee/LibraryWeb/build.gradle new file mode 100644 index 0000000..9a41aae --- /dev/null +++ b/java-ee/LibraryWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':LibraryImpl') +} diff --git a/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java b/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java new file mode 100644 index 0000000..9a566a0 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/java/com/peetz/library/view/LibraryView.java @@ -0,0 +1,42 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.library.view; + +import com.peetz.library.service.LibraryService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author tpeetz + */ +@ManagedBean(name="LibraryView") +@RequestScoped +public class LibraryView implements Serializable { + + private static final long serialVersionUID = -6251848426914654974L; + + private static final Logger LOG = Logger.getLogger(LibraryView.class.getName()); + + @EJB + private LibraryService libraryService; + + public LibraryView() { + LOG.info("LibraryView created"); + } + + public Integer getBookNumber() { + return libraryService.getAllBooks().size(); + } + + public Integer getArticleNumber() { + return libraryService.getAllArticles().size(); + } +} diff --git a/java-ee/LibraryWeb/src/main/webapp/index.jsp b/java-ee/LibraryWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..9e061a7 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + Library Application + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
Kontor +

Library Manager

+ Show the book list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp new file mode 100644 index 0000000..63f9266 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp new file mode 100644 index 0000000..d3ae5e5 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleEdit.jsp @@ -0,0 +1,69 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Titel:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp new file mode 100644 index 0000000..d8570ba --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/articleList.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show article list
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + <%-- check if article exists and display message or iterate over articles --%> + + + + + + + + + <%-- print out the article informations --%> + + <%-- print out the edit and delete link for each article --%> + + + + + + <%-- end interate --%> + + <%-- if articles cannot be found display a text --%> + + + + + + + +
Article Title  
No articles available
EditDelete
No articles found.
+
+ <%-- add and back to menu button --%> + Add a new article + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp new file mode 100644 index 0000000..e1bc491 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/boardAdd.jsp @@ -0,0 +1,68 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + +
Shelf:
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp new file mode 100644 index 0000000..7221317 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/boardEdit.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp new file mode 100644 index 0000000..d3fb02b --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp new file mode 100644 index 0000000..8d54239 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookEdit.jsp @@ -0,0 +1,85 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + + + + + + + + + +
Titel:
Autor:
Verlag:
ISBN:
Seiten:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp new file mode 100644 index 0000000..9c1d40d --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/bookList.jsp @@ -0,0 +1,33 @@ + + Library Application + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
Kontor +

Library Manager

+ +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp new file mode 100644 index 0000000..e75087a --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/index.jsp @@ -0,0 +1,58 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> +<%@taglib uri="http://java.sun.com/jstl/core" prefix="c"%> +<%@taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %> + + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + +

Library Manager

+ Show the booklist +
+ Import CD List +
+ + + Import Books + +
+ Show the bookshelfs +
+ Show the articles +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp new file mode 100644 index 0000000..30b7e3b --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp new file mode 100644 index 0000000..a7c56af --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfEdit.jsp @@ -0,0 +1,92 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Library Manager
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Title:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ + + + + + + + + + + + + + + + + + + +
Shelfboard  
No boards available
EditDelete
No boards available
Add board
+
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp new file mode 100644 index 0000000..3313853 --- /dev/null +++ b/java-ee/LibraryWeb/src/main/webapp/jsp/shelfList.jsp @@ -0,0 +1,91 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Library Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show shelf list
+ <% out.println(com.peetz.library.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + + <%-- check if bookshelf exists and display message or iterate over bookshelfs --%> + + + + + + + + + <%-- print out the book informations --%> + + + <%-- print out the edit and delete link for each book --%> + + + + + + <%-- end interate --%> + + <%-- if books cannot be found display a text --%> + + + + + + + +
Bookshelf name# Shelfs  
No shelfs available
EditDelete
No shelfs found.
+
+ <%-- add and back to menu button --%> + Add a new book shelf +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienImpl/build.gradle b/java-ee/MedienImpl/build.gradle new file mode 100644 index 0000000..a08e29c --- /dev/null +++ b/java-ee/MedienImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'Medien', 'Implementation-Version': version + } +} diff --git a/java-ee/MedienImpl/config/checkstyle/checkstyle.xml b/java-ee/MedienImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/MedienImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl b/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/MedienImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/MedienImpl/config/findbugs/findbugs.xml b/java-ee/MedienImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/MedienImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java new file mode 100644 index 0000000..a144dbc --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/dal/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.dal; \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java new file mode 100644 index 0000000..63cadc8 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/AudioCDEntity.java @@ -0,0 +1,91 @@ +package com.peetz.medien.entity; + +import java.util.Collection; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="AudioCD.findAll", query="SELECT c from AudioCDEntity as c"), + @NamedQuery(name="AudioCD.findByAlbum", query="SELECT c from AudioCDEntity as c WHERE c.album = :album") +}) + +@Entity +@Table(name="AUDIOCD") +public class AudioCDEntity +{ + private Long id; + private String album; + private String artist; + private String category; + private String releaseYear; + private Boolean wantList; + private Collection songs; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + @Column + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + @Column + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + @Column + public String getReleaseYear() { + return releaseYear; + } + + public void setReleaseYear(String releaseYear) { + this.releaseYear = releaseYear; + } + + @Column + public Boolean getWantList() { + return wantList; + } + + public void setWantList(Boolean wantList) { + this.wantList = wantList; + } + + @Column + public Collection getSongs() { + return songs; + } + + public void setSongs(Collection songs) { + this.songs = songs; + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java new file mode 100644 index 0000000..ad3bba9 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/BoxSetEntity.java @@ -0,0 +1,43 @@ +package com.peetz.medien.entity; + +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BOXSET") +public class BoxSetEntity +{ + private Long id; + private String title; + private Collection films; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @OneToMany(mappedBy="boxSet", cascade=CascadeType.REMOVE) + public Collection getFilms() { + return films; + } + + public void setFilms(Collection films) { + this.films = films; + } + +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java new file mode 100644 index 0000000..7be93e0 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/FilmEntity.java @@ -0,0 +1,50 @@ +package com.peetz.medien.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Film.findAll", query="SELECT f from FilmEntity as f"), + @NamedQuery(name="Film.findByTitle", query="SELECT f from FilmEntity as f WHERE f.title = :title") +}) + +@Entity +@Table(name="FILM") +public class FilmEntity +{ + private Long id; + private String title; + private BoxSetEntity boxSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public BoxSetEntity getBoxSet() { + return boxSet; + } + + public void setBoxSet(BoxSetEntity boxSet) { + this.boxSet = boxSet; + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java new file mode 100644 index 0000000..803ef9c --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/entity/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.entity; \ No newline at end of file diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java new file mode 100644 index 0000000..a1fc92a --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienService.java @@ -0,0 +1,22 @@ +package com.peetz.medien.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; + +@Local +public interface MedienService +{ + public Collection getAllCDs(); + public AudioCDEntity addCD(String title); + public Collection getAllDVDs(); + public FilmEntity addDVD(String title); + public Collection getAllBoxSets(); + public BoxSetEntity addBoxSet(String title); + public BoxSetEntity assignBoxSet(BoxSetEntity boxSet, FilmEntity film); + public void saveCD(AudioCDEntity audioCD); +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java new file mode 100644 index 0000000..9e08e58 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/MedienServiceImpl.java @@ -0,0 +1,75 @@ +package com.peetz.medien.service; + +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.medien.entity.AudioCDEntity; +import com.peetz.medien.entity.BoxSetEntity; +import com.peetz.medien.entity.FilmEntity; +import java.util.ArrayList; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="MedienService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class MedienServiceImpl implements MedienService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllCDs() { + Query query = em.createNamedQuery("AudioCD.findAll"); + ArrayList cdList = new ArrayList(query.getResultList()); + return cdList; + } + + @Override + public AudioCDEntity addCD(String title) { + // TODO Auto-generated method stub + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllDVDs() { + Query query = em.createNamedQuery("Film.findAll"); + ArrayList filmList = new ArrayList(query.getResultList()); + return filmList; + } + + @Override + public FilmEntity addDVD(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getAllBoxSets() { + // TODO Auto-generated method stub + return null; + } + + @Override + public BoxSetEntity addBoxSet(String title) { + // TODO Auto-generated method stub + return null; + } + + @Override + public BoxSetEntity assignBoxSet(BoxSetEntity boxSet, FilmEntity film) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void saveCD(AudioCDEntity audioCD) { + // TODO Auto-generated method stub + + } +} diff --git a/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java new file mode 100644 index 0000000..ab20de0 --- /dev/null +++ b/java-ee/MedienImpl/src/main/java/com/peetz/medien/service/package-info.java @@ -0,0 +1,8 @@ +/** + * + */ +/** + * @author TPEETZ + * + */ +package com.peetz.medien.service; \ No newline at end of file diff --git a/java-ee/MedienWeb/build.gradle b/java-ee/MedienWeb/build.gradle new file mode 100644 index 0000000..73a53fa --- /dev/null +++ b/java-ee/MedienWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':MedienImpl') +} diff --git a/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java b/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java new file mode 100644 index 0000000..ef14235 --- /dev/null +++ b/java-ee/MedienWeb/src/main/java/com/peetz/medien/view/MedienView.java @@ -0,0 +1,36 @@ +package com.peetz.medien.view; + +import com.peetz.medien.service.MedienService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="MedienView") +@RequestScoped +public class MedienView implements Serializable { + + private static final Logger LOG = Logger.getLogger(MedienView.class.getName()); + + @EJB + private MedienService medienService; + + private static final long serialVersionUID = -8261128991042235283L; + + public MedienView() { + LOG.info("MedienView created"); + } + + public Integer getCdNumber() { + return medienService.getAllCDs().size(); + } + + public Integer getDvdNumber() { + return medienService.getAllDVDs().size(); + } +} diff --git a/java-ee/MedienWeb/src/main/webapp/index.jsp b/java-ee/MedienWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..3e682f8 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + +
Medien Manager
test +

Medien Manager

+ Show the media list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp new file mode 100644 index 0000000..f61357f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdAdd.jsp @@ -0,0 +1,75 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + +
Category:
Album:
Artist:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp new file mode 100644 index 0000000..f61357f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdEdit.jsp @@ -0,0 +1,75 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + + + + + + + + + +
Category:
Album:
Artist:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp new file mode 100644 index 0000000..ad936ab --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/cdList.jsp @@ -0,0 +1,94 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + + + <%-- check if book exists and display message or iterate over cds --%> + + + + + + + + + <%-- print out the book informations --%> + + + + <%-- print out the edit and delete link for each CD --%> + + + + + + <%-- end interate --%> + + <%-- if cds cannot be found display a text --%> + + + + + + + +
CategoryAlbum titleArtist  
No CDs available
EditDelete
No CDs found.
+
+ <%-- add and back to menu button --%> + Add a new CD + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp new file mode 100644 index 0000000..e9d6617 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdAdd.jsp @@ -0,0 +1,67 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Film:
+ <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp new file mode 100644 index 0000000..9044d68 --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdEdit.jsp @@ -0,0 +1,69 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show CD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + <%-- create a html form --%> + + <%-- print out the form data --%> + + + + + + + +
Film:
+ <%-- hidden fields for id and userId --%> + + <%-- set the parameter for the dispatch action --%> + + +
+ <%-- submit and back button --%> + + Back + +   + Save +
+
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp new file mode 100644 index 0000000..6853fcd --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/dvdList.jsp @@ -0,0 +1,90 @@ +<%@ page language="java"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %> + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Show DVD list
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + + + + <%-- set the header --%> + + + + + + <%-- check if book exists and display message or iterate over books --%> + + + + + + + + + <%-- print out the DVD informations --%> + + <%-- print out the edit and delete link for each DVD --%> + + + + + + <%-- end interate --%> + + <%-- if dvds cannot be found display a text --%> + + + + + + + +
DVD title  
No DVDs available
EditDelete
No DVDs found.
+
+ <%-- add and back to menu button --%> + Add a new DVD + +   + Back to menu + + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp b/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp new file mode 100644 index 0000000..2b8b61f --- /dev/null +++ b/java-ee/MedienWeb/src/main/webapp/jsp/index.jsp @@ -0,0 +1,58 @@ +<%@ page language="java" contentType="text/html; charset=ISO-8859-1" + pageEncoding="ISO-8859-1"%> +<%@taglib uri="http://java.sun.com/jstl/core" prefix="c"%> +<%@taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %> + + + + + + + + + Medien Application + + + + + + + + + + + + + + + + + + + + + + + + +
Medien Manager (CD, DVD)
+ <% out.println(com.peetz.medien.navigation.MenuLinks.getInstance().toString()); %> + +

Medien Manager

+

CD Liste

+

Import CD List

+ + + Import CDs + +

DVD Liste

+

Import DVD List

+ + + Import DVDs + +
 
+

Ingenieurbüro Thomas Peetz

+
+ +
diff --git a/java-ee/README.md b/java-ee/README.md new file mode 100644 index 0000000..91ce4c6 --- /dev/null +++ b/java-ee/README.md @@ -0,0 +1,2 @@ +# Kontor Java Enterprise Edition + diff --git a/java-ee/TradingCardsImpl/build.gradle b/java-ee/TradingCardsImpl/build.gradle new file mode 100644 index 0000000..6e6daf4 --- /dev/null +++ b/java-ee/TradingCardsImpl/build.gradle @@ -0,0 +1,5 @@ +jar { + manifest { + attributes 'Implementation-Title': 'TradingCards', 'Implementation-Version': version + } +} diff --git a/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/TradingCardsImpl/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml b/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/TradingCardsImpl/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..e216bb2 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,31 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.List; +import javax.ejb.Local; + +/** + * + * @author tpeetz + */ +@Local +public interface ManufacturerDao { + public ManufacturerEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..a072a99 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,67 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.List; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +@Stateless(name = "ManufacturerDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class ManufacturerImpl implements ManufacturerDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public ManufacturerEntity getById(Long id) { + Query q = em.createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + ManufacturerEntity entity = (ManufacturerEntity)q.getSingleResult(); + return entity; + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = em.createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + List resultList = q.getResultList(); + return resultList; + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..badd03c --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportDao.java @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.SportEntity; +import java.util.List; +import javax.ejb.Local; + +/** + * + * @author tpeetz + */ +@Local +public interface SportDao { + public SportEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..059f52f --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/dal/SportImpl.java @@ -0,0 +1,58 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.SportEntity; +import java.util.List; +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +@Stateless(name = "SportDao") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class SportImpl implements SportDao { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = em.createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + List resultList = query.getResultList(); + return resultList; + } + + @Override + public SportEntity store(SportEntity entity) { + em.persist(entity); + return entity; + } + + @Override + public void delete(SportEntity entity) { + em.remove(entity); + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..30a23ac --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..7c44c92 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..4d73b06 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,74 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..2057456 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..2d34b16 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..b4e1a0b --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/PositionEntity.java @@ -0,0 +1,53 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..fd0e5e4 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..7574a2d --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/SportEntity.java @@ -0,0 +1,64 @@ +package com.peetz.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..a0863ea --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/entity/TeamEntity.java @@ -0,0 +1,42 @@ +package com.peetz.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + public void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java new file mode 100644 index 0000000..805101a --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportService.java @@ -0,0 +1,24 @@ +package com.peetz.tradingcards.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; + +@Local +public interface SportService { + + Collection getAllSports(); + + Collection getAllTeams(); + + void addSport(String name); + + Collection getTeams(SportEntity sport); + + Collection getPositions(SportEntity sport); + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java new file mode 100644 index 0000000..1a348ef --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/SportServiceImpl.java @@ -0,0 +1,63 @@ +package com.peetz.tradingcards.service; + +import com.peetz.tradingcards.dal.SportDao; +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.tradingcards.entity.PositionEntity; +import com.peetz.tradingcards.entity.SportEntity; +import com.peetz.tradingcards.entity.TeamEntity; +import java.util.ArrayList; +import javax.ejb.EJB; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="SportService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class SportServiceImpl implements SportService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @EJB + SportDao sportDao; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllSports() { + Query query = em.createNamedQuery("Sport.findAll"); + ArrayList sportList = new ArrayList(query.getResultList()); + return sportList; + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllTeams() { + Query query = em.createNamedQuery("Team.findAll"); + ArrayList teamList = new ArrayList(query.getResultList()); + return teamList; + } + + + @Override + public void addSport(String name) { + SportEntity entity = new SportEntity(); + entity.setName(name); + sportDao.store(entity); + } + + @Override + public Collection getTeams(SportEntity sport) { + return null; + } + + @Override + public Collection getPositions(SportEntity sport) { + // TODO Auto-generated method stub + return null; + } +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java new file mode 100644 index 0000000..9d5b0eb --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardService.java @@ -0,0 +1,28 @@ +package com.peetz.tradingcards.service; + +import java.util.Collection; + +import javax.ejb.Local; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.SportCardEntity; + +@Local +public interface TradingcardService { + + Collection getAllManufacturers(); + + void addManufacturer(String name); + + Collection getAllSportCards(); + + Collection getBaseSetsByManufacturer(ManufacturerEntity manufacturer); + + Collection getParallelSetsByManufacturer(ManufacturerEntity manufacturer); + + Collection getInsertsByManufacturer(ManufacturerEntity manufacturer); + +} diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java new file mode 100644 index 0000000..b8703a4 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/TradingcardServiceImpl.java @@ -0,0 +1,75 @@ +package com.peetz.tradingcards.service; + +import com.peetz.tradingcards.dal.ManufacturerDao; +import java.util.Collection; + +import javax.ejb.Stateless; +import javax.ejb.TransactionAttribute; +import javax.ejb.TransactionAttributeType; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.InsertEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import com.peetz.tradingcards.entity.ParallelSetEntity; +import com.peetz.tradingcards.entity.SportCardEntity; +import java.util.ArrayList; +import java.util.List; +import javax.ejb.EJB; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; + +@Stateless(name="TradingcardService") +@TransactionAttribute(TransactionAttributeType.REQUIRED) +public class TradingcardServiceImpl implements TradingcardService { + + @PersistenceContext(unitName = "kontor") + private EntityManager em; + + @EJB + ManufacturerDao manufacturerDao; + + @SuppressWarnings("unchecked") + @Override + public Collection getAllManufacturers() { + Query query = em.createNamedQuery("Manufacturer.findAll"); + ArrayList manufacturerList = new ArrayList(query.getResultList()); + return manufacturerList; + } + + @Override + public void addManufacturer(String name) { + List resultList = manufacturerDao.findByName(name); + if (resultList.isEmpty()) { + ManufacturerEntity manufacturer = new ManufacturerEntity(); + manufacturer.setName(name); + manufacturerDao.store(manufacturer); + } + } + + @SuppressWarnings("unchecked") + @Override + public Collection getAllSportCards() { + Query query = em.createNamedQuery("SportCard.findAll"); + ArrayList sportCardList = new ArrayList(query.getResultList()); + return sportCardList; + } + + @Override + public Collection getBaseSetsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getParallelSetsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Collection getInsertsByManufacturer(ManufacturerEntity manufacturer) { + // TODO Auto-generated method stub + return null; + } +} \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java new file mode 100644 index 0000000..f0eb37b --- /dev/null +++ b/java-ee/TradingCardsImpl/src/main/java/com/peetz/tradingcards/service/package-info.java @@ -0,0 +1,5 @@ +/** + * @author TPEETZ + * + */ +package com.peetz.tradingcards.service; \ No newline at end of file diff --git a/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java b/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..2e5b5e1 --- /dev/null +++ b/java-ee/TradingCardsImpl/src/test/java/com/peetz/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,147 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package com.peetz.tradingcards.dal; + +import com.peetz.tradingcards.entity.BaseSetEntity; +import com.peetz.tradingcards.entity.ManufacturerEntity; +import java.util.Collection; +import java.util.List; +import javax.ejb.embeddable.EJBContainer; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * + * @author tpeetz + */ +public class ManufacturerImplTest { + + public ManufacturerImplTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * Test of getById method, of class ManufacturerImpl. + */ + @Test + public void testGetById() throws Exception { + System.out.println("getById"); + Long id = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/main/ManufacturerDao"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.getById(id); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of findByIds method, of class ManufacturerImpl. + */ + @Test + public void testFindByIds() throws Exception { + System.out.println("findByIds"); + List ids = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + Collection expResult = null; + Collection result = instance.findByIds(ids); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of findByName method, of class ManufacturerImpl. + */ + @Test + public void testFindByName() throws Exception { + System.out.println("findByName"); + String name = ""; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + Collection expResult = null; + Collection result = instance.findByName(name); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of assignBaseSet method, of class ManufacturerImpl. + */ + @Test + public void testAssignBaseSet() throws Exception { + System.out.println("assignBaseSet"); + ManufacturerEntity comic = null; + BaseSetEntity baseSet = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.assignBaseSet(comic, baseSet); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of store method, of class ManufacturerImpl. + */ + @Test + public void testStore() throws Exception { + System.out.println("store"); + ManufacturerEntity entity = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + ManufacturerEntity expResult = null; + ManufacturerEntity result = instance.store(entity); + assertEquals(expResult, result); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + + /** + * Test of delete method, of class ManufacturerImpl. + */ + @Test + public void testDelete() throws Exception { + System.out.println("delete"); + ManufacturerEntity entity = null; + EJBContainer container = javax.ejb.embeddable.EJBContainer.createEJBContainer(); + ManufacturerDao instance = (ManufacturerDao)container.getContext().lookup("java:global/classes/ManufacturerImpl"); + instance.delete(entity); + container.close(); + // TODO review the generated test code and remove the default call to fail. + fail("The test case is a prototype."); + } + +} diff --git a/java-ee/TradingCardsWeb/build.gradle b/java-ee/TradingCardsWeb/build.gradle new file mode 100644 index 0000000..baa6136 --- /dev/null +++ b/java-ee/TradingCardsWeb/build.gradle @@ -0,0 +1,7 @@ +apply plugin: 'war' + +version = '0.0.1' + +dependencies { + compile project(':TradingCardsImpl') +} diff --git a/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java new file mode 100644 index 0000000..b95ca0b --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/SportView.java @@ -0,0 +1,52 @@ +package com.peetz.tradingcards.view; + +import com.peetz.tradingcards.dal.SportDao; +import com.peetz.tradingcards.service.SportService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author tpeetz + */ +@ManagedBean(name="TradingCardsView") +@RequestScoped +public class SportView implements Serializable { + + private static final long serialVersionUID = 1399103334723066025L; + + private static final Logger LOG = Logger.getLogger(SportView.class.getName()); + + private String name; + + @EJB + private SportService sportService; + + public SportView() { + LOG.info("SportView created"); + } + + public Integer getSportNumber() { + LOG.info("SportView#getSportNumber"); + return sportService.getAllSports().size(); + } + + public Integer getTeamNumber() { + LOG.info("SportView#getTeamNumber"); + return sportService.getAllTeams().size(); + } + + public String getName() { + LOG.info("SportView#getName"); + return name; + } + + public void setName(String name) { + this.name = name; + sportService.addSport(name); + LOG.info("SportView#setName"); + } +} diff --git a/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java new file mode 100644 index 0000000..e1d5879 --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/java/com/peetz/tradingcards/view/TradingCardsView.java @@ -0,0 +1,36 @@ +package com.peetz.tradingcards.view; + +import com.peetz.tradingcards.service.TradingcardService; +import java.io.Serializable; +import java.util.logging.Logger; +import javax.ejb.EJB; +import javax.faces.bean.ManagedBean; +import javax.faces.bean.RequestScoped; + +/** + * + * @author TPEETZ + */ +@ManagedBean(name="TradingCardsView") +@RequestScoped +public class TradingCardsView implements Serializable { + + private static final Logger LOG = Logger.getLogger(TradingCardsView.class.getName()); + + @EJB + private TradingcardService tradingcardService; + + private static final long serialVersionUID = -8261128991042235283L; + + public TradingCardsView() { + LOG.info("TradingCardsView created"); + } + + public Integer getManufacturerNumber() { + return tradingcardService.getAllManufacturers().size(); + } + + public Integer getSportCardNumber() { + return tradingcardService.getAllSportCards().size(); + } +} diff --git a/java-ee/TradingCardsWeb/src/main/webapp/index.jsp b/java-ee/TradingCardsWeb/src/main/webapp/index.jsp new file mode 100644 index 0000000..d085601 --- /dev/null +++ b/java-ee/TradingCardsWeb/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ + + TradingCards Application + + + + + + + + + + + + + + + + + + + + + + + +
TradingCards Manager
test +

TradingCards Manager

+ Show the card list +
 
+

Ingenieurbüro Thomas Peetz

+
+ + diff --git a/java-ee/build.gradle b/java-ee/build.gradle new file mode 100644 index 0000000..e8b1bd8 --- /dev/null +++ b/java-ee/build.gradle @@ -0,0 +1,69 @@ +allprojects { + apply plugin: 'java' + apply plugin: 'build-dashboard' + + repositories { + mavenLocal() + mavenCentral() + } + version = '0.0.1' +} + +repositories { + mavenLocal() + mavenCentral() +} + +group = 'com.ibtp.kontor' + +dependencies { + compile 'org.jboss.spec.javax.faces:jboss-jsf-api_2.2_spec:+' + compile 'org.hibernate.ogm:hibernate-ogm-mongodb:4.2.+' + compile "ch.qos.logback:logback-classic:1.1.3" + compile "org.slf4j:log4j-over-slf4j:1.7.13" + compile "javax:javaee-api:7.0" +} + +subprojects { project -> + if (project.name.endsWith('Impl')) { + apply plugin: 'checkstyle' + apply plugin: 'findbugs' + apply plugin: 'pmd' + apply plugin: 'jacoco' + dependencies { + compile 'org.glassfish.main.ejb:javax.ejb:3.1.2.2' + compile 'org.glassfish.main.transaction:javax.transaction:3.1.2.2' + compile 'org.glassfish:javax.faces:2.1.6' + compile 'org.eclipse.persistence:javax.persistence:2.1.0' + compile 'org.eclipse.persistence:eclipselink:2.5.1' + compile 'org.hibernate:hibernate-core:4.3.8.Final' + compile 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + compile 'org.hsqldb:hsqldb:2.3.0' + compile 'ch.qos.logback:logback-core:1.1.2' + compile 'ch.qos.logback:logback-classic:1.1.2' + testCompile 'org.glassfish.main.extras:glassfish-embedded-all:3.1.2.2' + testCompile group: 'junit', name: 'junit', version: '4.11' + } + tasks.withType(Checkstyle) { + ignoreFailures = true + showViolations = false + reports { + xml.enabled true + } + } + tasks.withType(FindBugs) { + reports { + xml.enabled true + } + } + pmd { + ignoreFailures = true + } + build.dependsOn(['jacocoTestReport']) + } +} + +wrapper { + gradleVersion = '3.3' +} + diff --git a/java-ee/comics.xml b/java-ee/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/java-ee/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/config/checkstyle/checkstyle.xml b/java-ee/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java-ee/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-ee/config/checkstyle/checkstyle.xsl b/java-ee/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java-ee/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java-ee/config/findbugs/findbugs.xml b/java-ee/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java-ee/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-ee/gradle/wrapper/gradle-wrapper.jar b/java-ee/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8ba921dbd16c876b27af7388dcbd710bdd096612 GIT binary patch literal 54208 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/java-ee/gradlew.bat b/java-ee/gradlew.bat new file mode 100755 index 0000000..e95643d --- /dev/null +++ b/java-ee/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem 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= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-ee/settings.gradle b/java-ee/settings.gradle new file mode 100755 index 0000000..50ec7b4 --- /dev/null +++ b/java-ee/settings.gradle @@ -0,0 +1,13 @@ +include ':KontorEJB' +include ':KontorWeb' +include ':ComicsImpl' +include ':ComicsWeb' +include ':LibraryImpl' +include ':LibraryWeb' +include ':MedienImpl' +include ':MedienWeb' +include ':TradingCardsImpl' +include ':TradingCardsWeb' +include ':KontorImpl' +include ':KontorWeb' +include ':KontorApp' diff --git a/java-quarkus/.gitignore b/java-quarkus/.gitignore new file mode 100644 index 0000000..cec4d77 --- /dev/null +++ b/java-quarkus/.gitignore @@ -0,0 +1,9 @@ +build/ +.settings/ +.gradle/ +.classpath +.project +.vscode/ +bin/ +.idea/ +/.eclipse-pmd diff --git a/java-quarkus/.gitlab-ci.yml b/java-quarkus/.gitlab-ci.yml new file mode 100644 index 0000000..ed4caf6 --- /dev/null +++ b/java-quarkus/.gitlab-ci.yml @@ -0,0 +1,38 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + - chmod +x ./gradlew + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew --build-cache compileJava + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish + diff --git a/java-quarkus/README.md b/java-quarkus/README.md new file mode 100644 index 0000000..11ec472 --- /dev/null +++ b/java-quarkus/README.md @@ -0,0 +1,60 @@ +# kontor-quarkus Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-quarkus-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/java-quarkus/build.gradle b/java-quarkus/build.gradle new file mode 100644 index 0000000..57e8102 --- /dev/null +++ b/java-quarkus/build.gradle @@ -0,0 +1,158 @@ +plugins { + id 'java' + id 'maven-publish' + id 'jacoco' + id 'jacoco-report-aggregation' + id 'pmd' + id 'checkstyle' + id 'jvm-test-suite' + id 'io.quarkus' + id "org.sonarqube" version "3.3" + alias(versionsLibs.plugins.asciidoctorConvert) + alias(versionsLibs.plugins.asciidoctorPdf) + alias(versionsLibs.plugins.asciidoctorGems) +} + +repositories { + ruby.gems() + maven { + url "https://nexus.thpeetz.de/nexus/content/groups/public/" + } + mavenCentral() + mavenLocal() +} + +dependencies { + implementation enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}") + implementation 'io.quarkus:quarkus-resteasy-reactive' + implementation 'io.quarkus:quarkus-resteasy-reactive-jackson' + implementation 'io.quarkus:quarkus-arc' + testImplementation 'io.quarkus:quarkus-junit5' + testImplementation 'io.rest-assured:rest-assured' + testImplementation("io.quarkus:quarkus-jacoco") +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/" + project.name + ".pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +publishing { + publications { + quarkusApp(MavenPublication){ + from components.java + } + docs(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + } + repositories { + maven { + name = 'nexusPeetz' + def releasesRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/releases/" + def snapshotsRepoUrl = "https://nexus.thpeetz.de/nexus/content/repositories/snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + credentials(PasswordCredentials) + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GenerateModuleMetadata).configureEach { + suppressedValidationErrors.add('enforced-platform') +} + +compileJava { + options.encoding = 'UTF-8' + options.compilerArgs << '-parameters' +} + +compileTestJava { + options.encoding = 'UTF-8' +} + +jacocoTestReport { + reports { + xml.required = true + } +} + +test.finalizedBy jacocoTestReport + + +pmd { + //ruleSetFiles = files("custom-pmd-ruleset.xml") + ruleSets = ["category/java/errorprone.xml", "category/java/bestpractices.xml"] + ignoreFailures = true +} + +tasks.withType(Checkstyle) { + reports { + xml.required = true + html.required = true + } +} + +testing { + suites { + test { + useJUnitJupiter() + } + } +} + +pmdTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +checkstyleTest.dependsOn("compileQuarkusTestGeneratedSourcesJava") +jacocoTestReport.dependsOn("compileQuarkusGeneratedSourcesJava") +pmdMain.dependsOn("compileQuarkusGeneratedSourcesJava") + +sonarqube { + properties { + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + property "sonar.projectKey", "kontor_kontor-quarkus_AYQek8Mxz0hBjLSV8I8O" + property "sonar.login", "5ecd90dee57806857e07443a9b0efd3cd7774a81" + } +} + +tasks.named('sonarqube').configure { + dependsOn 'test', 'pmdMain', 'checkstyleMain', 'jacocoTestReport' +} + +asciidoctorPdf { + baseDirFollowsSourceFile() + + asciidoctorj { + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/java-quarkus/config/checkstyle/checkstyle.xml b/java-quarkus/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..2574b06 --- /dev/null +++ b/java-quarkus/config/checkstyle/checkstyle.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java-quarkus/gradle.properties b/java-quarkus/gradle.properties new file mode 100644 index 0000000..c30b39f --- /dev/null +++ b/java-quarkus/gradle.properties @@ -0,0 +1,9 @@ +#Gradle properties +group=de.thpeetz.kontor +version=1.0.0-SNAPSHOT + +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.13.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.13.2.Final \ No newline at end of file diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.jar b/java-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/java-quarkus/gradle/wrapper/gradle-wrapper.properties b/java-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/java-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java-quarkus/gradlew b/java-quarkus/gradlew new file mode 100644 index 0000000..fbd7c51 --- /dev/null +++ b/java-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java-quarkus/gradlew.bat b/java-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/java-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java-quarkus/settings.gradle b/java-quarkus/settings.gradle new file mode 100644 index 0000000..0ab2682 --- /dev/null +++ b/java-quarkus/settings.gradle @@ -0,0 +1,11 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id "${quarkusPluginId}" version "${quarkusPluginVersion}" + } +} +rootProject.name='kontor-quarkus' diff --git a/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc new file mode 100644 index 0000000..d5d7ac4 --- /dev/null +++ b/java-quarkus/src/docs/asciidoc/kontor-quarkus.adoc @@ -0,0 +1,84 @@ += Projektbeschreibung kontor-quarkus: Pflichtenheft und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: article +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +//[title="Dokumenthistorie", caption="Tabelle {counter:table-number} ", id="Tabelle-{counter:table-number}", options="header"] +//[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header", cols="4"] +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 0.0.1 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +[bibliography] +== Referenzen + +[glossary] +== Glossar + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/java-quarkus/src/main/docker/Dockerfile.jvm b/java-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..476b644 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-quarkus-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/java-quarkus/src/main/docker/Dockerfile.legacy-jar b/java-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..8f309c6 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-quarkus-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/java-quarkus/src/main/docker/Dockerfile.native b/java-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..d5a28b2 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/docker/Dockerfile.native-micro b/java-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..55df6f9 --- /dev/null +++ b/java-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor-quarkus . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-quarkus +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java new file mode 100644 index 0000000..f6518b9 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/ComicsResource.java @@ -0,0 +1,95 @@ +package de.thpeetz.kontor.comics; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import de.thpeetz.kontor.comics.service.ArtistService; +import de.thpeetz.kontor.comics.service.ComicsService; +import de.thpeetz.kontor.comics.service.PublisherService; + +@Path("/kontor/comics") +public class ComicsResource { + + @Inject + transient ComicsService comicsService; + + @Inject + transient ArtistService artistService; + + @Inject + transient PublisherService publisherService; + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/") + public String showComicList() { + return comicsService.showComicList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist") + public String showArtistList() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/artist") + public String showArtistListJson() { + return artistService.showArtistList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/view/{artist_id}") + public String showArtist(String artist_id) { + return artistService.showArtist(artist_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String showArtistCreation() { + return artistService.showArtistCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/artist/create") + public String validateArtistDetails() { + return artistService.validateArtistDetails(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher") + public String showPublisherList() { + return publisherService.showPublisherList(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/view/{publisher_id}") + public String showPublisher(String publisher_id) { + return publisherService.showPublisher(publisher_id); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String showPublisherCreation() { + return publisherService.showPublisherCreation(); + } + + @POST + @Produces(MediaType.TEXT_HTML) + @Path("/publisher/create") + public String validatePublisherDetails() { + return publisherService.validatePublisherDetails(); + } +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java new file mode 100644 index 0000000..cdef9aa --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ArtistService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ArtistService { + + public String showArtistList() { + // TODO Auto-generated method stub + return null; + } + + public String showArtist(String artist_id) { + // TODO Auto-generated method stub + return null; + } + + public String showArtistCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validateArtistDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java new file mode 100644 index 0000000..ee58e17 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/ComicsService.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ComicsService { + + public String showComicList() { + return "comics"; + } + +} diff --git a/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java new file mode 100644 index 0000000..d3414b1 --- /dev/null +++ b/java-quarkus/src/main/java/de/thpeetz/kontor/comics/service/PublisherService.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.comics.service; + +import javax.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class PublisherService { + + public String showPublisherList() { + // TODO Auto-generated method stub + return null; + } + + public String showPublisher(String publisher_id) { + // TODO Auto-generated method stub + return null; + } + + public String showPublisherCreation() { + // TODO Auto-generated method stub + return null; + } + + public String validatePublisherDetails() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/java-quarkus/src/main/resources/META-INF/resources/index.html b/java-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..609ae3a --- /dev/null +++ b/java-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,284 @@ + + + + + kontor-quarkus - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive (guide)
  • +
  • RESTEasy Reactive Jackson
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/java-quarkus/src/main/resources/application.properties b/java-quarkus/src/main/resources/application.properties new file mode 100644 index 0000000..21328a1 --- /dev/null +++ b/java-quarkus/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Your configuration properties +quarkus.http.test-port=8083 +quarkus.http.test-ssl-port=8446 diff --git a/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java new file mode 100644 index 0000000..230d951 --- /dev/null +++ b/java-quarkus/src/native-test/java/de/thpeetz/kontor/comics/ComicsResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.comics; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class ComicsResourceIT extends ComicsResourceTest { + +} diff --git a/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java new file mode 100644 index 0000000..25dcd49 --- /dev/null +++ b/java-quarkus/src/test/java/de/thpeetz/kontor/comics/ComicsResourceTest.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics; + +import org.junit.jupiter.api.Test; +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ComicsResourceTest { + + @Test + void testComicsEndpoint() { + given().when().get("/kontor/comics") + .then() + .statusCode(200) + .body(is("comics")); + } +} diff --git a/java/README.md b/java/README.md new file mode 100644 index 0000000..65ed57c --- /dev/null +++ b/java/README.md @@ -0,0 +1,2 @@ +# Kontor Java + diff --git a/java/build.gradle b/java/build.gradle new file mode 100644 index 0000000..1a683be --- /dev/null +++ b/java/build.gradle @@ -0,0 +1,87 @@ +plugins { + id 'application' + id 'jacoco' + id "org.sonarqube" version "3.3" +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + implementation 'ch.qos.logback:logback-core:1.1.2' + implementation 'ch.qos.logback:logback-classic:1.1.2' + implementation 'org.mongodb.morphia:morphia:1.1.0' + //compile 'org.mongodb:mongodb-driver:3.2.2' + implementation 'org.hibernate:hibernate-core:4.3.8.Final' + implementation 'org.hibernate:hibernate-entitymanager:4.3.8.Final' + implementation 'org.hsqldb:hsqldb:2.3.0' + implementation 'ch.qos.logback:logback-core:1.1.2' + implementation 'ch.qos.logback:logback-classic:1.1.2' + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") +} + +def MAIN_CLASS_NAME = 'com.ibtp.kontor.KontorApp' + +application { + mainClassName = MAIN_CLASS_NAME +} + +jar { + manifest { + attributes('Implementation-Title': 'Kontor Application', 'Implementation-Version': version, 'Main-Class': MAIN_CLASS_NAME) + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-java_AX-dd-w3rXuu6JVRvr_g" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "d39622f640a91f501b1e8a73d7d78c4fc412fc98" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +//tasks.withType(Checkstyle) { +// ignoreFailures = true +// showViolations = false +// configFile = rootProject.file("config/checkstyle/checkstyle.xml") +// reports { +// xml.enabled true +// } +//} + +//tasks.withType(FindBugs) { +// ignoreFailures = true +// reports { +// xml.enabled true +// } +//} + +//pmd { +// ignoreFailures = true +//} + +//build.dependsOn(['jacocoTestReport']) + +wrapper { + gradleVersion = "6.3" +} diff --git a/java/comics.xml b/java/comics.xml new file mode 100644 index 0000000..730fdeb --- /dev/null +++ b/java/comics.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/config/checkstyle/checkstyle.xml b/java/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..7c682c3 --- /dev/null +++ b/java/config/checkstyle/checkstyle.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/config/checkstyle/checkstyle.xsl b/java/config/checkstyle/checkstyle.xsl new file mode 100644 index 0000000..393a01b --- /dev/null +++ b/java/config/checkstyle/checkstyle.xsl @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

CheckStyle Audit

Designed for use with CheckStyle and Ant.
+
+ + + +
+ + + +
+ + + + + +

+

+ +


+ + + + +
+ + + + +

Files

+ + + + + + + + + + + + + + +
NameErrors
+
+ + + + +

File

+ + + + + + + + + + + + + +
Error DescriptionLine
+ Back to top +
+ + + +

Summary

+ + + + + + + + + + + + +
FilesErrors
+
+ + + + a + b + + +
+ + diff --git a/java/config/findbugs/findbugs.xml b/java/config/findbugs/findbugs.xml new file mode 100644 index 0000000..34a6e01 --- /dev/null +++ b/java/config/findbugs/findbugs.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/gradle.properties b/java/gradle.properties new file mode 100644 index 0000000..4facf35 --- /dev/null +++ b/java/gradle.properties @@ -0,0 +1,2 @@ +description = 'Application kontor-java' +version = '1.0.0-SNAPSHOT' diff --git a/java/gradle/wrapper/gradle-wrapper.jar b/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ zw`1S}(D?mUXu_C{wupRS-jiX~w=Uqqhf|Vb3Cm9L=T+w91Cu^ z*&Ty%sN?x*h~mJc4g~k{xD4ZmF%FXZNC;oVDwLZ_WvrnzY|{v8hc1nmx4^}Z;yriXsAf+Lp+OFLbR!&Ox?xABwl zu8w&|5pCxmu#$?Cv2_-Vghl2LZ6m7}VLEfR5o2Ou$x02uA-%QB2$c(c1rH3R9hesc zfpn#oqpbKuVsdfV#cv@5pV4^f_!WS+F>SV6N0JQ9E!T90EX((_{bSSFv9ld%I0&}9 zH&Jd4MEX1e0iqDtq~h?DBrxQX1iI0lIs<|kB$Yrh&cpeK0-^K%=FBsCBT46@h#yi!AyDq1V(#V}^;{{V*@T4WJ&U-NTq43w=|K>z8%pr_nC>%C(Wa_l78Ufib$r8Od)IIN=u>417 z`Hl{9A$mI5A(;+-Q&$F&h-@;NR>Z<2U;Y21>>Z;s@0V@SbkMQQj%_;~+qTuQ?c|AV zcWm3XZQHhP&R%QWarS%mJ!9R^&!_)*s(v+VR@I#QrAT}`17Y+l<`b-nvmDNW`De%y zrwTZ9EJrj1AFA>B`1jYDow}~*dfPs}IZMO3=a{Fy#IOILc8F0;JS4x(k-NSpbN@qM z`@aE_e}5{!$v3+qVs7u?sOV(y@1Os*Fgu`fCW9=G@F_#VQ%xf$hj0~wnnP0$hFI+@ zkQj~v#V>xn)u??YutKsX>pxKCl^p!C-o?+9;!Nug^ z{rP!|+KsP5%uF;ZCa5F;O^9TGac=M|=V z_H(PfkV1rz4jl?gJ(ArXMyWT4y(86d3`$iI4^l9`vLdZkzpznSd5Ikfrs8qcSy&>z zTIZgWZGXw0n9ibQxYWE@gI0(3#KA-dAdPcsL_|hg2@~C!VZDM}5;v_Nykfq!*@*Zf zE_wVgx82GMDryKO{U{D>vSzSc%B~|cjDQrt5BN=Ugpsf8H8f1lR4SGo#hCuXPL;QQ z#~b?C4MoepT3X`qdW2dNn& zo8)K}%Lpu>0tQei+{>*VGErz|qjbK#9 zvtd8rcHplw%YyQCKR{kyo6fgg!)6tHUYT(L>B7er5)41iG`j$qe*kSh$fY!PehLcD zWeKZHn<492B34*JUQh=CY1R~jT9Jt=k=jCU2=SL&&y5QI2uAG2?L8qd2U(^AW#{(x zThSy=C#>k+QMo^7caQcpU?Qn}j-`s?1vXuzG#j8(A+RUAY})F@=r&F(8nI&HspAy4 z4>(M>hI9c7?DCW8rw6|23?qQMSq?*Vx?v30U%luBo)B-k2mkL)Ljk5xUha3pK>EEj z@(;tH|M@xkuN?gsz;*bygizwYR!6=(Xgcg^>WlGtRYCozY<rFX2E>kaZo)O<^J7a`MX8Pf`gBd4vrtD|qKn&B)C&wp0O-x*@-|m*0egT=-t@%dD zgP2D+#WPptnc;_ugD6%zN}Z+X4=c61XNLb7L1gWd8;NHrBXwJ7s0ce#lWnnFUMTR& z1_R9Fin4!d17d4jpKcfh?MKRxxQk$@)*hradH2$3)nyXep5Z;B z?yX+-Bd=TqO2!11?MDtG0n(*T^!CIiF@ZQymqq1wPM_X$Iu9-P=^}v7npvvPBu!d$ z7K?@CsA8H38+zjA@{;{kG)#AHME>Ix<711_iQ@WWMObXyVO)a&^qE1GqpP47Q|_AG zP`(AD&r!V^MXQ^e+*n5~Lp9!B+#y3#f8J^5!iC@3Y@P`;FoUH{G*pj*q7MVV)29+j z>BC`a|1@U_v%%o9VH_HsSnM`jZ-&CDvbiqDg)tQEnV>b%Ptm)T|1?TrpIl)Y$LnG_ zzKi5j2Fx^K^PG1=*?GhK;$(UCF-tM~^=Z*+Wp{FSuy7iHt9#4n(sUuHK??@v+6*|10Csdnyg9hAsC5_OrSL;jVkLlf zHXIPukLqbhs~-*oa^gqgvtpgTk_7GypwH><53riYYL*M=Q@F-yEPLqQ&1Sc zZB%w}T~RO|#jFjMWcKMZccxm-SL)s_ig?OC?y_~gLFj{n8D$J_Kw%{r0oB8?@dWzn zB528d-wUBQzrrSSLq?fR!K%59Zv9J4yCQhhDGwhptpA5O5U?Hjqt>8nOD zi{)0CI|&Gu%zunGI*XFZh(ix)q${jT8wnnzbBMPYVJc4HX*9d^mz|21$=R$J$(y7V zo0dxdbX3N#=F$zjstTf*t8vL)2*{XH!+<2IJ1VVFa67|{?LP&P41h$2i2;?N~RA30LV`BsUcj zfO9#Pg1$t}7zpv#&)8`mis3~o+P(DxOMgz-V*(?wWaxi?R=NhtW}<#^Z?(BhSwyar zG|A#Q7wh4OfK<|DAcl9THc-W4*>J4nTevsD%dkj`U~wSUCh15?_N@uMdF^Kw+{agk zJ`im^wDqj`Ev)W3k3stasP`88-M0ZBs7;B6{-tSm3>I@_e-QfT?7|n0D~0RRqDb^G zyHb=is;IwuQ&ITzL4KsP@Z`b$d%B0Wuhioo1CWttW8yhsER1ZUZzA{F*K=wmi-sb#Ju+j z-l@In^IKnb{bQG}Ps>+Vu_W#grNKNGto+yjA)?>0?~X`4I3T@5G1)RqGUZuP^NJCq&^HykuYtMDD8qq+l8RcZNJsvN(10{ zQ1$XcGt}QH-U^WU!-wRR1d--{B$%vY{JLWIV%P4-KQuxxDeJaF#{eu&&r!3Qu{w}0f--8^H|KwE>)ORrcR+2Qf zb})DRcH>k0zWK8@{RX}NYvTF;E~phK{+F;MkIP$)T$93Ba2R2TvKc>`D??#mv9wg$ zd~|-`Qx5LwwsZ2hb*Rt4S9dsF%Cny5<1fscy~)d;0m2r$f=83<->c~!GNyb!U)PA; zq^!`@@)UaG)Ew(9V?5ZBq#c%dCWZrplmuM`o~TyHjAIMh0*#1{B>K4po-dx$Tk-Cq z=WZDkP5x2W&Os`N8KiYHRH#UY*n|nvd(U>yO=MFI-2BEp?x@=N<~CbLJBf6P)}vLS?xJXYJ2^<3KJUdrwKnJnTp{ zjIi|R=L7rn9b*D#Xxr4*R<3T5AuOS+#U8hNlfo&^9JO{VbH!v9^JbK=TCGR-5EWR@ zN8T-_I|&@A}(hKeL4_*eb!1G8p~&_Im8|wc>Cdir+gg90n1dw?QaXcx6Op_W1r=axRw>4;rM*UOpT#Eb9xU1IiWo@h?|5uP zka>-XW0Ikp@dIe;MN8B01a7+5V@h3WN{J=HJ*pe0uwQ3S&MyWFni47X32Q7SyCTNQ z+sR!_9IZa5!>f&V$`q!%H8ci!a|RMx5}5MA_kr+bhtQy{-^)(hCVa@I!^TV4RBi zAFa!Nsi3y37I5EK;0cqu|9MRj<^r&h1lF}u0KpKQD^5Y+LvFEwM zLU@@v4_Na#Axy6tn3P%sD^5P#<7F;sd$f4a7LBMk zGU^RZHBcxSA%kCx*eH&wgA?Qwazm8>9SCSz_!;MqY-QX<1@p$*T8lc?@`ikEqJ>#w zcG``^CoFMAhdEXT9qt47g0IZkaU)4R7wkGs^Ax}usqJ5HfDYAV$!=6?>J6+Ha1I<5 z|6=9soU4>E))tW$<#>F ziZ$6>KJf0bPfbx_)7-}tMINlc=}|H+$uX)mhC6-Hz+XZxsKd^b?RFB6et}O#+>Wmw9Ec9) z{q}XFWp{3@qmyK*Jvzpyqv57LIR;hPXKsrh{G?&dRjF%Zt5&m20Ll?OyfUYC3WRn{cgQ?^V~UAv+5 z&_m#&nIwffgX1*Z2#5^Kl4DbE#NrD&Hi4|7SPqZ}(>_+JMz=s|k77aEL}<=0Zfb)a z%F(*L3zCA<=xO)2U3B|pcTqDbBoFp>QyAEU(jMu8(jLA61-H!ucI804+B!$E^cQQa z)_ERrW3g!B9iLb3nn3dlkvD7KsY?sRvls3QC0qPi>o<)GHx%4Xb$5a3GBTJ(k@`e@ z$RUa^%S15^1oLEmA=sayrP5;9qtf!Z1*?e$ORVPsXpL{jL<6E)0sj&swP3}NPmR%FM?O>SQgN5XfHE< zo(4#Cv11(%Nnw_{_Ro}r6=gKd{k?NebJ~<~Kv0r(r0qe4n3LFx$5%x(BKvrz$m?LG zjLIc;hbj0FMdb9aH9Lpsof#yG$(0sG2%RL;d(n>;#jb!R_+dad+K;Ccw!|RY?uS(a zj~?=&M!4C(5LnlH6k%aYvz@7?xRa^2gml%vn&eKl$R_lJ+e|xsNfXzr#xuh(>`}9g zLHSyiFwK^-p!;p$yt7$F|3*IfO3Mlu9e>Dpx8O`37?fA`cj`C0B-m9uRhJjs^mRp# zWB;Aj6|G^1V6`jg7#7V9UFvnB4((nIwG?k%c7h`?0tS8J3Bn0t#pb#SA}N-|45$-j z$R>%7cc2ebAClXc(&0UtHX<>pd)akR3Kx_cK+n<}FhzmTx!8e9^u2e4%x{>T6pQ`6 zO182bh$-W5A3^wos0SV_TgPmF4WUP-+D25KjbC{y_6W_9I2_vNKwU(^qSdn&>^=*t z&uvp*@c8#2*paD!ZMCi3;K{Na;I4Q35zw$YrW5U@Kk~)&rw;G?d7Q&c9|x<Hg|CNMsxovmfth*|E*GHezPTWa^Hd^F4!B3sF;)? z(NaPyAhocu1jUe(!5Cy|dh|W2=!@fNmuNOzxi^tE_jAtzNJ0JR-avc_H|ve#KO}#S z#a(8secu|^Tx553d4r@3#6^MHbH)vmiBpn0X^29xEv!Vuh1n(Sr5I0V&`jA2;WS|Y zbf0e}X|)wA-Pf5gBZ>r4YX3Mav1kKY(ulAJ0Q*jB)YhviHK)w!TJsi3^dMa$L@^{` z_De`fF4;M87vM3Ph9SzCoCi$#Fsd38u!^0#*sPful^p5oI(xGU?yeYjn;Hq1!wzFk zG&2w}W3`AX4bxoVm03y>ts{KaDf!}b&7$(P4KAMP=vK5?1In^-YYNtx1f#}+2QK@h zeSeAI@E6Z8a?)>sZ`fbq9_snl6LCu6g>o)rO;ijp3|$vig+4t} zylEo7$SEW<_U+qgVcaVhk+4k+C9THI5V10qV*dOV6pPtAI$)QN{!JRBKh-D zk2^{j@bZ}yqW?<#VVuI_27*cI-V~sJiqQv&m07+10XF+#ZnIJdr8t`9s_EE;T2V;B z4UnQUH9EdX%zwh-5&wflY#ve!IWt0UE-My3?L#^Bh%kcgP1q{&26eXLn zTkjJ*w+(|_>Pq0v8{%nX$QZbf)tbJaLY$03;MO=Ic-uqYUmUCuXD>J>o6BCRF=xa% z3R4SK9#t1!K4I_d>tZgE>&+kZ?Q}1qo4&h%U$GfY058s%*=!kac{0Z+4Hwm!)pFLR zJ+5*OpgWUrm0FPI2ib4NPJ+Sk07j(`diti^i#kh&f}i>P4~|d?RFb#!JN)~D@)beox}bw?4VCf^y*`2{4`-@%SFTry2h z>9VBc9#JxEs1+0i2^LR@B1J`B9Ac=#FW=(?2;5;#U$0E0UNag_!jY$&2diQk_n)bT zl5Me_SUvqUjwCqmVcyb`igygB_4YUB*m$h5oeKv3uIF0sk}~es!{D>4r%PC*F~FN3owq5e0|YeUTSG#Vq%&Gk7uwW z0lDo#_wvflqHeRm*}l?}o;EILszBt|EW*zNPmq#?4A+&i0xx^?9obLyY4xx=Y9&^G;xYXYPxG)DOpPg!i_Ccl#3L}6xAAZzNhPK1XaC_~ z!A|mlo?Be*8Nn=a+FhgpOj@G7yYs(Qk(8&|h@_>w8Y^r&5nCqe0V60rRz?b5%J;GYeBqSAjo|K692GxD4` zRZyM2FdI+-jK2}WAZTZ()w_)V{n5tEb@>+JYluDozCb$fA4H)$bzg(Ux{*hXurjO^ zwAxc+UXu=&JV*E59}h3kzQPG4M)X8E*}#_&}w*KEgtX)cU{vm9b$atHa;s>| z+L6&cn8xUL*OSjx4YGjf6{Eq+Q3{!ZyhrL&^6Vz@jGbI%cAM9GkmFlamTbcQGvOlL zmJ?(FI)c86=JEs|*;?h~o)88>12nXlpMR4@yh%qdwFNpct;vMlc=;{FSo*apJ;p}! zAX~t;3tb~VuP|ZW;z$=IHf->F@Ml)&-&Bnb{iQyE#;GZ@C$PzEf6~q}4D>9jic@mTO5x76ulDz@+XAcm35!VSu zT*Gs>;f0b2TNpjU_BjHZ&S6Sqk6V1370+!eppV2H+FY!q*n=GHQ!9Rn6MjY!Jc77A zG7Y!lFp8?TIHN!LXO?gCnsYM-gQxsm=Ek**VmZu7vnuufD7K~GIxfxbsQ@qv2T zPa`tvHB$fFCyZl>3oYg?_wW)C>^_iDOc^B7klnTOoytQH18WkOk)L2BSD0r%xgRSW zQS9elF^?O=_@|58zKLK;(f77l-Zzu}4{fXed2saq!5k#UZAoDBqYQS{sn@j@Vtp|$ zG%gnZ$U|9@u#w1@11Sjl8ze^Co=)7yS(}=;68a3~g;NDe_X^}yJj;~s8xq9ahQ5_r zxAlTMnep*)w1e(TG%tWsjo3RR;yVGPEO4V{Zp?=a_0R#=V^ioQu4YL=BO4r0$$XTX zZfnw#_$V}sDAIDrezGQ+h?q24St0QNug_?{s-pI(^jg`#JRxM1YBV;a@@JQvH8*>> zIJvku74E0NlXkYe_624>znU0J@L<-c=G#F3k4A_)*;ky!C(^uZfj%WB3-*{*B$?9+ zDm$WFp=0(xnt6`vDQV3Jl5f&R(Mp};;q8d3I%Kn>Kx=^;uSVCw0L=gw53%Bp==8Sw zxtx=cs!^-_+i{2OK`Q;913+AXc_&Z5$@z3<)So0CU3;JAv=H?@Zpi~riQ{z-zLtVL z!oF<}@IgJp)Iyz1zVJ42!SPHSkjYNS4%ulVVIXdRuiZ@5Mx8LJS}J#qD^Zi_xQ@>DKDr-_e#>5h3dtje*NcwH_h;i{Sx7}dkdpuW z(yUCjckQsagv*QGMSi9u1`Z|V^}Wjf7B@q%j2DQXyd0nOyqg%m{CK_lAoKlJ7#8M} z%IvR?Vh$6aDWK2W!=i?*<77q&B8O&3?zP(Cs@kapc)&p7En?J;t-TX9abGT#H?TW? ztO5(lPKRuC7fs}zwcUKbRh=7E8wzTsa#Z{a`WR}?UZ%!HohN}d&xJ=JQhpO1PI#>X zHkb>pW04pU%Bj_mf~U}1F1=wxdBZu1790>3Dm44bQ#F=T4V3&HlOLsGH)+AK$cHk6 zia$=$kog?)07HCL*PI6}DRhpM^*%I*kHM<#1Se+AQ!!xyhcy6j7`iDX7Z-2i73_n# zas*?7LkxS-XSqv;YBa zW_n*32D(HTYQ0$feV_Fru1ZxW0g&iwqixPX3=9t4o)o|kOo79V$?$uh?#8Q8e>4e)V6;_(x&ViUVxma+i25qea;d-oK7ouuDsB^ab{ zu1qjQ%`n56VtxBE#0qAzb7lph`Eb-}TYpXB!H-}3Ykqyp`otprp7{VEuW*^IR2n$Fb99*nAtqT&oOFIf z@w*6>YvOGw@Ja?Pp1=whZqydzx@9X4n^2!n83C5{C?G@|E?&$?p*g68)kNvUTJ)I6 z1Q|(#UuP6pj78GUxq11m-GSszc+)X{C2eo-?8ud9sB=3(D47v?`JAa{V(IF zPZQ_0AY*9M97>Jf<o%#O_%Wq}8>YM=q0|tGY+hlXcpE=Z4Od z`NT7Hu2hnvRoqOw@g1f=bv`+nba{GwA$Ak0INlqI1k<9!x_!sL()h?hEWoWrdU3w` zZ%%)VR+Bc@_v!C#koM1p-3v_^L6)_Ktj4HE>aUh%2XZE@JFMOn)J~c`_7VWNb9c-N z2b|SZMR4Z@E7j&q&9(6H3yjEu6HV7{2!1t0lgizD;mZ9$r(r7W5G$ky@w(T_dFnOD z*p#+z$@pKE+>o@%eT(2-p_C}wbQ5s(%Sn_{$HDN@MB+Ev?t@3dPy`%TZ!z}AThZSu zN<1i$siJhXFdjV zP*y|V<`V8t=h#XTRUR~5`c`Z9^-`*BZf?WAehGdg)E2Je)hqFa!k{V(u+(hTf^Yq& zoruUh2(^3pe)2{bvt4&4Y9CY3js)PUHtd4rVG57}uFJL)D(JfSIo^{P=7liFXG zq5yqgof0V8paQcP!gy+;^pp-DA5pj=gbMN0eW=-eY+N8~y+G>t+x}oa!5r>tW$xhI zPQSv=pi;~653Gvf6~*JcQ%t1xOrH2l3Zy@8AoJ+wz@daW@m7?%LXkr!bw9GY@ns3e zSfuWF_gkWnesv?s3I`@}NgE2xwgs&rj?kH-FEy82=O8`+szN ziHch`vvS`zNfap14!&#i9H@wF7}yIPm=UB%(o(}F{wsZ(wA0nJ2aD^@B41>>o-_U6 zUqD~vdo48S8~FTb^+%#zcbQiiYoDKYcj&$#^;Smmb+Ljp(L=1Kt_J!;0s%1|JK}Wi z;={~oL!foo5n8=}rs6MmUW~R&;SIJO3TL4Ky?kh+b2rT9B1Jl4>#Uh-Bec z`Hsp<==#UEW6pGPhNk8H!!DUQR~#F9jEMI6T*OWfN^Ze&X(4nV$wa8QUJ>oTkruH# zm~O<`J7Wxseo@FqaZMl#Y(mrFW9AHM9Kb|XBMqaZ2a)DvJgYipkDD_VUF_PKd~dT7 z#02}bBfPn9a!X!O#83=lbJSK#E}K&yx-HI#T6ua)6o0{|={*HFusCkHzs|Fn&|C3H zBck1cmfcWVUN&i>X$YU^Sn6k2H;r3zuXbJFz)r5~3$d$tUj(l1?o={MM){kjgqXRO zc5R*#{;V7AQh|G|)jLM@wGAK&rm2~@{Pewv#06pHbKn#wL0P6F1!^qw9g&cW3Z=9} zj)POhOlwsh@eF=>z?#sIs*C-Nl(yU!#DaiaxhEs#iJqQ8w%(?+6lU02MYSeDkr!B- zPjMv+on6OLXgGnAtl(ao>|X2Y8*Hb}GRW5}-IzXnoo-d0!m4Vy$GS!XOLy>3_+UGs z2D|YcQx@M#M|}TDOetGi{9lGo9m-=0-^+nKE^*?$^uHkxZh}I{#UTQd;X!L+W@jm( zDg@N4+lUqI92o_rNk{3P>1gxAL=&O;x)ZT=q1mk0kLlE$WeWuY_$0`0jY-Kkt zP*|m3AF}Ubd=`<>(Xg0har*_@x2YH}bn0Wk*OZz3*e5;Zc;2uBdnl8?&XjupbkOeNZsNh6pvsq_ydmJI+*z**{I{0K)-;p1~k8cpJXL$^t!-`E}=*4G^-E8>H!LjTPxSx zcF+cS`ommfKMhNSbas^@YbTpH1*RFrBuATUR zt{oFWSk^$xU&kbFQ;MCX22RAN5F6eq9UfR$ut`Jw--p2YX)A*J69m^!oYfj2y7NYcH6&r+0~_sH^c^nzeN1AU4Ga7=FlR{S|Mm~MpzY0$Z+p2W(a={b-pR9EO1Rs zB%KY|@wLcAA@)KXi!d2_BxrkhDn`DT1=Dec}V!okd{$+wK z4E{n8R*xKyci1(CnNdhf$Dp2(Jpof0-0%-38X=Dd9PQgT+w%Lshx9+loPS~MOm%ZT zt%2B2iL_KU_ita%N>xjB!#71_3=3c}o zgeW~^U_ZTJQ2!PqXulQd=3b=XOQhwATK$y(9$#1jOQ4}4?~l#&nek)H(04f(Sr=s| zWv7Lu1=%WGk4FSw^;;!8&YPM)pQDCY9DhU`hMty1@sq1=Tj7bFsOOBZOFlpR`W>-J$-(kezWJj;`?x-v>ev{*8V z8p|KXJPV$HyQr1A(9LVrM47u-XpcrIyO`yWvx1pVYc&?154aneRpLqgx)EMvRaa#|9?Wwqs2+W8n5~79G z(}iCiLk;?enn}ew`HzhG+tu+Ru@T+K5juvZN)wY;x6HjvqD!&!)$$;1VAh~7fg0K| zEha#aN=Yv|3^~YFH}cc38ovVb%L|g@9W6fo(JtT6$fa?zf@Ct88e}m?i)b*Jgc{fl zExfdvw-BYDmH6>(4QMt#p0;FUIQqkhD}aH?a7)_%JtA~soqj{ppP_82yi9kaxuK>~ ze_)Zt>1?q=ZH*kF{1iq9sr*tVuy=u>Zev}!gEZx@O6-fjyu9X00gpIl-fS_pzjpqJ z1yqBmf9NF!jaF<+YxgH6oXBdK)sH(>VZ)1siyA$P<#KDt;8NT*l_0{xit~5j1P)FN zI8hhYKhQ)i z37^aP13B~u65?sg+_@2Kr^iWHN=U;EDSZ@2W2!5ALhGNWXnFBY%7W?1 z=HI9JzQ-pLKZDYTv<0-lt|6c-RwhxZ)mU2Os{bsX_i^@*fKUj8*aDO5pks=qn3Dv6 zwggpKLuyRCTVPwmw1r}B#AS}?X7b837UlXwp~E2|PJw2SGVueL7){Y&z!jL!XN=0i zU^Eig`S2`{+gU$68aRdWx?BZ{sU_f=8sn~>s~M?GU~`fH5kCc; z8ICp+INM3(3{#k32RZdv6b9MQYdZXNuk7ed8;G?S2nT+NZBG=Tar^KFl2SvhW$bGW#kdWL-I)s_IqVnCDDM9fm8g;P;8 z7t4yZn3^*NQfx7SwmkzP$=fwdC}bafQSEF@pd&P8@H#`swGy_rz;Z?Ty5mkS%>m#% zp_!m9e<()sfKiY(nF<1zBz&&`ZlJf6QLvLhl`_``%RW&{+O>Xhp;lwSsyRqGf=RWd zpftiR`={2(siiPAS|p}@q=NhVc0ELprt%=fMXO3B)4ryC2LT(o=sLM7hJC!}T1@)E zA3^J$3&1*M6Xq>03FX`R&w*NkrZE?FwU+Muut;>qNhj@bX17ZJxnOlPSZ=Zeiz~T_ zOu#yc3t6ONHB;?|r4w+pI)~KGN;HOGC)txxiUN8#mexj+W(cz%9a4sx|IRG=}ia zuEBuba3AHsV2feqw-3MvuL`I+2|`Ud4~7ZkN=JZ;L20|Oxna5vx1qbIh#k2O4$RQF zo`tL()zxaqibg^GbB+BS5#U{@K;WWQj~GcB1zb}zJkPwH|5hZ9iH2308!>_;%msji zJHSL~s)YHBR=Koa1mLEOHos*`gp=s8KA-C zu0aE+W!#iJ*0xqKm3A`fUGy#O+X+5W36myS>Uh2!R*s$aCU^`K&KKLCCDkejX2p=5 z%o7-fl03x`gaSNyr?3_JLv?2RLS3F*8ub>Jd@^Cc17)v8vYEK4aqo?OS@W9mt%ITJ z9=S2%R8M){CugT@k~~0x`}Vl!svYqX=E)c_oU6o}#Hb^%G1l3BudxA{F*tbjG;W_>=xV73pKY53v%>I)@D36I_@&p$h|Aw zonQS`07z_F#@T-%@-Tb|)7;;anoD_WH>9ewFy(ZcEOM$#Y)8>qi7rCnsH9GO-_7zF zu*C87{Df1P4TEOsnzZ@H%&lvV(3V@;Q!%+OYRp`g05PjY^gL$^$-t0Y>H*CDDs?FZly*oZ&dxvsxaUWF!{em4{A>n@vpXg$dwvt@_rgmHF z-MER`ABa8R-t_H*kv>}CzOpz;!>p^^9ztHMsHL|SRnS<-y5Z*r(_}c4=fXF`l^-i}>e7v!qs_jv zqvWhX^F=2sDNWA9c@P0?lUlr6ecrTKM%pNQ^?*Lq?p-0~?_j50xV%^(+H>sMul#Tw zeciF*1=?a7cI(}352%>LO96pD+?9!fNyl^9v3^v&Y4L)mNGK0FN43&Xf8jUlxW1Bw zyiu2;qW-aGNhs=zbuoxnxiwZ3{PFZM#Kw)9H@(hgX23h(`Wm~m4&TvoZoYp{plb^> z_#?vXcxd>r7K+1HKJvhed>gtK`TAbJUazUWQY6T~t2af%#<+Veyr%7-#*A#@&*;@g58{i|E%6yC_InGXCOd{L0;$)z#?n7M`re zh!kO{6=>7I?*}czyF7_frt#)s1CFJ_XE&VrDA?Dp3XbvF{qsEJgb&OLSNz_5g?HpK z9)8rsr4JN!Af3G9!#Qn(6zaUDqLN(g2g8*M)Djap?WMK9NKlkC)E2|-g|#-rp%!Gz zAHd%`iq|81efi93m3yTBw3g0j#;Yb2X{mhRAI?&KDmbGqou(2xiRNb^sV}%%Wu0?< z?($L>(#BO*)^)rSgyNRni$i`R4v;GhlCZ8$@e^ROX(p=2_v6Y!%^As zu022)fHdv_-~Yu_H6WVPLpHQx!W%^6j)cBhS`O3QBW#x(eX54d&I22op(N59b*&$v zFiSRY6rOc^(dgSV1>a7-5C;(5S5MvKcM2Jm-LD9TGqDpP097%52V+0>Xqq!! zq4e3vj53SE6i8J`XcQB|MZPP8j;PAOnpGnllH6#Ku~vS42xP*Nz@~y%db7Xi8s09P z1)e%8ys6&M8D=Dt6&t`iKG_4X=!kgRQoh%Z`dc&mlOUqXk-k`jKv9@(a^2-Upw>?< zt5*^DV~6Zedbec4NVl($2T{&b)zA@b#dUyd>`2JC0=xa_fIm8{5um zr-!ApXZhC8@=vC2WyxO|!@0Km)h8ep*`^he92$@YwP>VcdoS5OC^s38e#7RPsg4j+ zbVGG}WRSET&ZfrcR(x~k8n1rTP%CnfUNKUonD$P?FtNFF#cn!wEIab-;jU=B1dHK@ z(;(yAQJ`O$sMn>h;pf^8{JISW%d+@v6@CnXh9n5TXGC}?FI9i-D0OMaIg&mAg=0Kn zNJ7oz5*ReJukD55fUsMuaP+H4tDN&V9zfqF@ zr=#ecUk9wu{0;!+gl;3Bw=Vn^)z$ahVhhw)io!na&9}LmWurLb0zubxK=UEnU*{5P z+SP}&*(iBKSO4{alBHaY^)5Q=mZ+2OwIooJ7*Q5XJ+2|q`9#f?6myq!&oz?klihLq z4C)$XP!BNS0G_Z1&TM>?Jk{S~{F3n83ioli=IO6f%wkvCl(RFFw~j0tb{GvXTx>*sB0McY0s&SNvj4+^h`9nJ_wM>F!Uc>X}9PifQekn0sKI2SAJP!a4h z5cyGTuCj3ZBM^&{dRelIlT^9zcfaAuL5Y~bl!ppSf`wZbK$z#6U~rdclk``e+!qhe z6Qspo*%<)eu6?C;Bp<^VuW6JI|Ncvyn+LlSl;Mp22Bl7ARQ0Xc24%29(ZrdsIPw&-=yHQ7_Vle|5h>AST0 zUGX2Zk34vp?U~IHT|;$U86T+UUHl_NE4m|}>E~6q``7hccCaT^#y+?wD##Q%HwPd8 zV3x4L4|qqu`B$4(LXqDJngNy-{&@aFBvVsywt@X^}iH7P%>bR?ciC$I^U-4Foa`YKI^qDyGK7k%E%c_P=yzAi`YnxGA%DeNd++j3*h^ z=rn>oBd0|~lZ<6YvmkKY*ZJlJ;Im0tqgWu&E92eqt;+NYdxx`eS(4Hw_Jb5|yVvBg z*tbdY^!AN;luEyN4VRhS@-_DC{({ziH{&Z}iGElSV~qvT>L-8G%+yEL zX#MFOhj{InyKG=mvW-<1B@c-}x$vA(nU?>S>0*eN#!SLzQ)Ex7fvQ)S4D<8|I#N$3 zT5Ei`Z?cxBODHX8(Xp73v`IsAYC@9b;t}z0wxVuQSY1J^GRwDPN@qbM-ZF48T$GZ< z8WU+;Pqo?{ghI-KZ-i*ydXu`Ep0Xw^McH_KE9J0S7G;x8Fe`DVG?j3Pv=0YzJ}yZR z%2=oqHiUjvuk0~Ca>Kol4CFi0_xQT~;_F?=u+!kIDl-9g`#ZNZ9HCy17Ga1v^Jv9# z{T4Kb1-AzUxq*MutfOWWZgD*HnFfyYg0&e9f(5tZ>krPF6{VikNeHoc{linPPt#Si z&*g>(c54V8rT_AX!J&bNm-!umPvOR}vDai#`CX___J#=zeB*{4<&2WpaDncZsOkp* zsg<%@@rbrMkR_ux9?LsQxzoBa1s%$BBn6vk#{&&zUwcfzeCBJUwFYSF$08qDsB;gWQN*g!p8pxjofWbqNSZOEKOaTx@+* zwdt5*Q47@EOZ~EZL9s?1o?A%9TJT=Ob_13yyugvPg*e&ZU(r6^k4=2+D-@n=Hv5vu zSXG|hM(>h9^zn=eQ=$6`JO&70&2|%V5Lsx>)(%#;pcOfu>*nk_3HB_BNaH$`jM<^S zcSftDU1?nL;jy)+sfonQN}(}gUW?d_ikr*3=^{G)=tjBtEPe>TO|0ddVB zTklrSHiW+!#26frPXQQ(YN8DG$PZo?(po(QUCCf_OJC`pw*uey00%gmH!`WJkrKXj2!#6?`T25mTu9OJp2L8z3! z=arrL$ZqxuE{%yV)14Kd>k}j7pxZ6#$Dz8$@WV5p8kTqN<-7W)Q7Gt2{KoOPK_tZ| zf2WG~O5@{qPI+W<4f_;reuFVdO^5`ADC1!JQE|N`s3cq@(0WB!n0uh@*c{=LAd;~} zyGK@hbF-Oo+!nN)@i*O(`@FA#u?o=~e{`4O#5}z&=UkU*50fOrzi11D^&FOqe>wii z?*k+2|EcUs;Gx{!@KBT~>PAwLrIDT7Th=Utu?~?np@t^gFs?zgX=D${RwOY^WGh-+ z+#4$066ISh8eYW#FXWp~S`<*%O^ZuItL1Tyqt8#tZ zY120E;^VG`!lZn&3sPd$RkdHpU#|w+bYV)pJC|SH9g%|5IkxVTQcBA4CL0}$&}ef@ zW^Vtj%M;;_1xxP9x#ex17&4N*{ksO*_4O}xYu(p*JkL#yr}@7b)t5X?%CY<+s5_MJ zuiqt+N_;A(_)%lumoyRFixWa-M7qK_9s6<1X?JDa9fP!+_6u~~M$5L=ipB=7(j#f< zZ34J%=bs549%~_mA(|={uZNs_0?o7;-LBP(ZRnkd{-^|2|=4vUTmtByHL8 zEph`(LSEzQj68a+`d$V<45J7cyv^#|^|%fD#si1Nx!4NW*`l*{->HEWNh6-|g>-=r zXmQ|-i}Ku$ndUeHQ^&ieT!Lf}vf6GaqW9$DJ2NWrqwPY%%4nip$@vK$nRp*_C-v<| zuKz~ZyN&<%!NS26&x?jhy+@awJipMQ-8(X4#Ae5??U<1QMt1l9R=w9fAnEF}NYu$2 z>6}Vkc zIb*A?G*z8^IvibmBKn_u^5&T_1oey0gZS2~obf(#xk=erZGTEdQnt3DMGM+0oPwss zj5zXD;(oWhB_T@~Ig#9@v)AKtXu3>Inmgf@A|-lD-1U>cNyl3h?ADD9)GG4}zUGPk zZzaXe!~Kf?<~@$G?Uql3t8jy9{2!doq4=J}j9ktTxss{p6!9UdjyDERlA*xZ!=Q)KDs5O)phz>Vq3BNGoM(H|=1*Q4$^2fTZw z(%nq1P|5Rt81}SYJpEEzMPl5VJsV5&4e)ZWKDyoZ>1EwpkHx-AQVQc8%JMz;{H~p{=FXV>jIxvm4X*qv52e?Y-f%DJ zxEA165GikEASQ^fH6K#d!Tpu2HP{sFs%E=e$gYd$aj$+xue6N+Wc(rAz~wUsk2`(b z8Kvmyz%bKQxpP}~baG-rwYcYCvkHOi zlkR<=>ZBTU*8RF_d#Bl@zZsRIhx<%~Z@Z=ik z>adw3!DK(8R|q$vy{FTxw%#xliD~6qXmY^7_9kthVPTF~Xy1CfBqbU~?1QmxmU=+k z(ggxvEuA;0e&+ci-zQR{-f7aO{O(Pz_OsEjLh_K>MbvoZ4nxtk5u{g@nPv)cgW_R} z9}EA4K4@z0?7ue}Z(o~R(X&FjejUI2g~08PH1E4w>9o{)S(?1>Z0XMvTb|;&EuyOE zGvWNpYX)Nv<8|a^;1>bh#&znEcl-r!T#pn= z4$?Yudha6F%4b>*8@=BdtXXY4N+`U4Dmx$}>HeVJk-QdTG@t!tVT#0(LeV0gvqyyw z2sEp^9eY0N`u10Tm4n8No&A=)IeEC|gnmEXoNSzu!1<4R<%-9kY_8~5Ej?zRegMn78wuMs#;i&eUA0Zk_RXQ3b&TT} z;SCI=7-FUB@*&;8|n>(_g^HGf3@QODE3LpmX~ELnymQm{Sx9xrKS zK29p~?v@R$0=v6Dr5aW>-!{+h@?Q58|Kz8{{W`%J+lDAdb&M5VHrX_mDY;1-JLnf)ezmPau$)1;=`-FU=-r-83tX=C`S#}GZufju zQ>sXNT0Ny=k@nc%cFnvA_i4SC)?_ORXHq8B4D%el1uPX`c~uG#S1M7C+*MMqLw78E zhY2dI8@+N^qrMI1+;TUda(vGqGSRyU{Fnm`aqrr7bz42c5xsOO-~oZpkzorD1g}Y<6rk&3>PsSGy}W?MtqFky@A(X# zIuNZK0cK?^=;PUAu>j0#HtjbHCV*6?jzA&OoE$*Jlga*}LF`SF?WLhv1O|zqC<>*> zYB;#lsYKx0&kH@BFpW8n*yDcc6?;_zaJs<-jPSkCsSX-!aV=P5kUgF@Nu<{a%#K*F z134Q{9|YX7X(v$62_cY3^G%t~rD>Q0z@)1|zs)vjJ6Jq9;7#Ki`w+eS**En?7;n&7 zu==V3T&eFboN3ZiMx3D8qYc;VjFUk_H-WWCau(VFXSQf~viH0L$gwD$UfFHqNcgN`x}M+YQ6RnN<+@t>JUp#)9YOkqst-Ga?{FsDpEeX0(5v{0J~SEbWiL zXC2}M4?UH@u&|;%0y`eb33ldo4~z-x8zY!oVmV=c+f$m?RfDC35mdQ2E>Pze7KWP- z>!Bh<&57I+O_^s}9Tg^k)h7{xx@0a0IA~GAOt2yy!X%Q$1rt~LbTB6@Du!_0%HV>N zlf)QI1&gvERKwso23mJ!Ou6ZS#zCS5W`gxE5T>C#E|{i<1D35C222I33?Njaz`On7 zi<+VWFP6D{e-{yiN#M|Jgk<44u1TiMI78S5W`Sdb5f+{zu34s{CfWN7a3Cf^@L%!& zN$?|!!9j2c)j$~+R6n#891w-z8(!oBpL2K=+%a$r2|~8-(vQj5_XT`<0Ksf;oP+tz z9CObS!0m)Tgg`K#xBM8B(|Z)Wb&DYL{WTYv`;A=q6~Nnx2+!lTIXtj8J7dZE!P_{z z#f8w6F}^!?^KE#+ZDv+xd5O&3EmomZzsv?>E-~ygGum45fk!SBN&|eo1rKw^?aZJ4 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/java/gradle/wrapper/gradle-wrapper.properties b/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a4b4429 --- /dev/null +++ b/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/java/gradlew b/java/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/java/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/java/gradlew.bat b/java/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/java/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/java/settings.gradle b/java/settings.gradle new file mode 100644 index 0000000..d7360d1 --- /dev/null +++ b/java/settings.gradle @@ -0,0 +1 @@ +rootProject.name='kontor-java' diff --git a/java/src/main/java/com/ibtp/kontor/Database.java b/java/src/main/java/com/ibtp/kontor/Database.java new file mode 100644 index 0000000..8d97843 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/Database.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor; + +import com.mongodb.MongoClient; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.Morphia; + +import java.util.HashMap; + +/** + * Created by thomas on 01.03.16. + */ +public class Database { + + private static Database instance = null; + + private HashMap dbMap; + private Morphia morphia; + + private Database() { + dbMap = new HashMap(); + initMorphia(); + } + + private final void initMorphia() { + morphia = new Morphia(); + morphia.mapPackage("com.ibtp.kontor.comics.entity"); + } + + public void registerDatastore(String databaseName) { + if (!dbMap.containsKey(databaseName)) { + Datastore store = morphia.createDatastore(new MongoClient("127.0.0.1"), databaseName); + dbMap.put(databaseName, store); + } + } + + public Datastore getDatastore(String databaseName) { + return dbMap.get(databaseName); + } + + public static Database init() { + if (instance == null) { + instance = new Database(); + } + return instance; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/DumpComics.java b/java/src/main/java/com/ibtp/kontor/DumpComics.java new file mode 100644 index 0000000..6ee56b1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/DumpComics.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor; + +import com.ibtp.kontor.comics.entity.Artist; +import com.ibtp.kontor.comics.entity.Comic; +import com.ibtp.kontor.comics.entity.Publisher; +import com.mongodb.MongoClient; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.Morphia; +import org.mongodb.morphia.query.Query; + +import java.util.Iterator; +import java.util.List; + +/** + * Created by thomas on 29.02.16. + */ +public class DumpComics { + + public static void main(String[] args) { + final Database database = Database.init(); + database.registerDatastore("kontor"); + database.registerDatastore("comics"); + Datastore kontor = database.getDatastore("kontor"); + Datastore comics = database.getDatastore("comics"); + final Query query = kontor.createQuery(Artist.class); + final List artists = query.asList(); + System.out.println(artists); + for (Artist artist: artists) { + String artistName = artist.getName(); + System.out.println("Artist(" + artist.getId() + ": " + artistName + ")"); + if (comics.createQuery(Artist.class).field("name").equal(artistName).asList().isEmpty()) { + Artist comicsArtist = new Artist(artistName); + comics.save(comicsArtist); + } + } + final List publishers = kontor.createQuery(Publisher.class).asList(); + for (Publisher publisher: publishers) { + String publisherName = publisher.getName(); + System.out.println("Publisher(" + publisher.getId() + ": " + publisherName + ")"); + if (comics.createQuery(Publisher.class).field("name").equal(publisherName).asList().isEmpty()) { + Publisher comicsPublisher = new Publisher(publisherName); + comics.save(comicsPublisher); + } + } + final List comicList = comics.createQuery(Comic.class).asList(); + System.out.println(comicList); + for (Comic comic: comicList ) { + System.out.println(comic); + } + } +} diff --git a/java/src/main/java/com/ibtp/kontor/KontorApp.java b/java/src/main/java/com/ibtp/kontor/KontorApp.java new file mode 100644 index 0000000..b87a849 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/KontorApp.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class KontorApp { + + private KontorGUI mainframe; + + public KontorApp() { + mainframe = new KontorGUI(this); + + mainframe.setVisible(true); + } + + public void exitApplication() { + System.exit(0); + } + + public static void main(String[] args) { + new KontorApp(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/KontorGUI.java b/java/src/main/java/com/ibtp/kontor/KontorGUI.java new file mode 100644 index 0000000..40a4b2d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/KontorGUI.java @@ -0,0 +1,69 @@ +package com.ibtp.kontor; + + +import com.ibtp.kontor.comics.view.ComicsMenu; +import com.ibtp.kontor.library.view.LibraryMenu; +import com.ibtp.kontor.tradingcards.view.TradingCardsMenu; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Created by TPEETZ on 11.02.2015. + */ +public class KontorGUI extends javax.swing.JFrame { + + KontorApp application; + JMenuBar menuBar; + JMenu menuFile; + JMenuItem menuFileExit; + + JMenuItem menuFileStart = new JMenuItem(); + + JMenu menuHelp; + JMenuItem menuHelpAbout; + + public KontorGUI(KontorApp kontorApp) { + application = kontorApp; + initComponents(); + } + + private void initComponents() { + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING).addGap(0, 300, Short.MAX_VALUE) + ); + pack(); + setTitle("Kontor Application"); + createMainMenu(); + //createToolBar(); + } + + private void createMainMenu() { + menuBar = new JMenuBar(); + menuFile = new JMenu("File"); + menuFileExit = new JMenuItem("Exit"); + menuHelp = new JMenu("Help"); + menuHelpAbout = new JMenuItem("About"); + setJMenuBar(menuBar); + menuBar.add(menuFile); + menuFile.add(menuFileExit); + menuBar.add(new ComicsMenu()); + menuBar.add(new LibraryMenu()); + menuBar.add(new TradingCardsMenu()); + menuBar.add(menuHelp); + menuHelp.add(menuHelpAbout); + menuFileExit.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + application.exitApplication(); + } + }); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java new file mode 100644 index 0000000..00c4b01 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +interface ArtistDao { + + public ArtistEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public ArtistEntity addArtist(String name); + + public ArtistEntity store(ArtistEntity entity); + + public void delete(ArtistEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java new file mode 100644 index 0000000..d01b987 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ArtistImpl.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class ArtistImpl extends BaseImpl implements ArtistDao { + + public ArtistImpl() {} + + @Override + public ArtistEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Artist.findById"); + query.setParameter("id", id); + return (ArtistEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Artist.findAll"); + return query.getResultList(); + + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Artist.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public ArtistEntity addArtist(String name) { + ArtistEntity artist = new ArtistEntity(name); + artist = store(artist); + return artist; + } + + @Override + public ArtistEntity store(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArtistEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java new file mode 100644 index 0000000..c77fb89 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface ComicDao { + + public ComicEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public ComicEntity addComic(String title); + + public ComicEntity store(ComicEntity entity); + + public void delete(ComicEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java new file mode 100644 index 0000000..ecbad88 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/ComicImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImpl extends BaseImpl implements ComicDao { + + @Override + public ComicEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Comic.findById"); + query.setParameter("id", id); + return (ComicEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Comic.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Comic.findAll"); + return query.getResultList(); + } + + @Override + public ComicEntity addComic(String title) { + ComicEntity comicEntity = new ComicEntity(); + comicEntity.setTitle(title); + store(comicEntity); + return comicEntity; + } + + @Override + public ComicEntity store(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ComicEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java new file mode 100644 index 0000000..49a5ab2 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface IssueDao { + public IssueEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByNumber(String number); + + public Collection findAll(); + + public IssueEntity store(IssueEntity entity); + + public void delete(IssueEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java new file mode 100644 index 0000000..8f70845 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/IssueImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImpl extends BaseImpl implements IssueDao { + + @Override + public IssueEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Issue.findById"); + query.setParameter("id", id); + return (IssueEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByNumber(String number) { + Query query = getEntityManager().createNamedQuery("Issue.findByNumber"); + query.setParameter("number", number); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Issue.findAll"); + return query.getResultList(); + } + + @Override + public IssueEntity store(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(IssueEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java new file mode 100644 index 0000000..0722522 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by thomas on 17.01.15. + */ +interface PublisherDao { + + public PublisherEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PublisherEntity addPublisher(String name); + + public PublisherEntity store(PublisherEntity entity); + + public void delete(PublisherEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java new file mode 100644 index 0000000..d911c4d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/PublisherImpl.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImpl extends BaseImpl implements PublisherDao { + + @Override + public PublisherEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Publisher.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Publisher.findAll"); + return query.getResultList(); + } + + @Override + public PublisherEntity addPublisher(String name) { + PublisherEntity publisher = new PublisherEntity(name); + store(publisher); + return publisher; + } + + @Override + public PublisherEntity store(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PublisherEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java new file mode 100644 index 0000000..40761fe --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface StoryArcDao { + + public StoryArcEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public StoryArcEntity store(StoryArcEntity entity); + + public void delete(StoryArcEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java new file mode 100644 index 0000000..3a39271 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/StoryArcImpl.java @@ -0,0 +1,55 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImpl extends BaseImpl implements StoryArcDao { + + @Override + public StoryArcEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("StoryArc.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("StoryArc.findAll"); + return query.getResultList(); + } + + @Override + public StoryArcEntity store(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(StoryArcEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java new file mode 100644 index 0000000..26759bd --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface VolumeDao { + + public VolumeEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByTitle(String title); + + public Collection findAll(); + + public VolumeEntity store(VolumeEntity entity); + + public void delete(VolumeEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java new file mode 100644 index 0000000..bb20a4a --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/dal/VolumeImpl.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.BaseImpl; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImpl extends BaseImpl implements VolumeDao { + + @Override + public VolumeEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Volume.findById"); + query.setParameter("id", id); + return (VolumeEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Volume.findByTitle"); + query.setParameter("title", title); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Volume.findAll"); + return query.getResultList(); + } + + @Override + public VolumeEntity store(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(VolumeEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java new file mode 100644 index 0000000..a7c09b9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Artist.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +/** + * Created by thomas on 29.02.16. + */ +@Entity("artist") +public class Artist { + + public Artist() {} + + public Artist(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java new file mode 100644 index 0000000..50ede83 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/ArtistEntity.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 16.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Artist.findAll", query="SELECT a from ArtistEntity as a"), + @NamedQuery(name="Artist.findByName", query="SELECT a from ArtistEntity as a WHERE a.name = :name") +}) + +@Entity +@Table(name="ARTIST") +public class ArtistEntity { + + private Long id; + + private String name; + + private Collection writtenIssues = new ArrayList(); + + private Collection inkedIssues = new ArrayList(); + + private Collection penciledIssues = new ArrayList(); + + public ArtistEntity(String name) { + setName(name); + } + + public ArtistEntity() {} + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setWrittenIssues(Collection writtenIssues) { this.writtenIssues = writtenIssues; } + + @OneToMany(mappedBy="writer", cascade=CascadeType.REMOVE) + public Collection getWrittenIssues() { + return writtenIssues; + } + + public void setInkedIssues(Collection inkedIssues) { this.inkedIssues = inkedIssues; } + + @OneToMany(mappedBy="inker", cascade=CascadeType.REMOVE) + public Collection getInkedIssues() { + return inkedIssues; + } + + public void setPenciledIssues(Collection penciledIssues) { this.penciledIssues = penciledIssues; } + + @OneToMany(mappedBy="penciler", cascade=CascadeType.REMOVE) + public Collection getPenciledIssues() { + return penciledIssues; + } + + @Override + public String toString() { + return "Artist[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java new file mode 100644 index 0000000..c6ac7b6 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Comic.java @@ -0,0 +1,98 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +@Entity("comic") +public class Comic { + + @Id + private ObjectId id; + + @Property + private String title; + + @Property + private ObjectId publisher; + + @Property + private Boolean current_order; + + @Property + private Boolean completed; + + @Property + private List issues = new ArrayList(); + + @Property + private List stories = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public ObjectId getPublisher() { + return publisher; + } + + public void setPublisher(ObjectId publisher) { + this.publisher = publisher; + } + + public Boolean getCurrent_order() { + return current_order; + } + + public void setCurrent_order(Boolean current_order) { + this.current_order = current_order; + } + + public Boolean getCompleted() { + return completed; + } + + public void setCompleted(Boolean completed) { + this.completed = completed; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } + + public List getStories() { + return stories; + } + + public void setStories(List stories) { + this.stories = stories; + } + + @Override + public String toString() { + return title + " - " + Publisher.getById(publisher); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java new file mode 100644 index 0000000..2d21c10 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/ComicEntity.java @@ -0,0 +1,81 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Comic.findAll", query="SELECT c from ComicEntity as c"), + @NamedQuery(name="Comic.findByTitle", query="SELECT c from ComicEntity as c WHERE c.title = :title") +}) +@Entity +@Table(name = "COMIC") +public class ComicEntity { + + private Long id; + + private String title; + + private Boolean completed; + + private Boolean currentOrder; + + private Collection issues = new ArrayList(); + + private Collection storyArc = new ArrayList(); + + private Collection volumes = new ArrayList(); + + private PublisherEntity publisher; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @Column + public Boolean getCompleted() { return completed; } + + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + @Column + public Boolean getCurrentOrder() { return currentOrder; } + + public Boolean isCurrentOrder() { return currentOrder; } + + public void setCurrentOrder(Boolean currentOrder) { this.currentOrder = currentOrder; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setStoryArc(Collection storyArc) { this.storyArc = storyArc; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getStoryArc() { return storyArc; } + + public void setVolumes(Collection volumes) { this.volumes = volumes; } + + @OneToMany(mappedBy="comic", cascade=CascadeType.REMOVE) + public Collection getVolumes() { return volumes; } + + @ManyToOne + public PublisherEntity getPublisher() { return publisher; } + + public void setPublisher(PublisherEntity publisher) { + this.publisher = publisher; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java new file mode 100644 index 0000000..9821134 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Issue.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +/** + * Created by thomas on 01.03.16. + */ +public class Issue { + + @Id + private ObjectId id; + + @Property + private String number; + + //@Reference + private Comic comic; + + @Property + private Boolean is_read; + + @Property + private Boolean is_stock; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public Boolean getIs_read() { + return is_read; + } + + public void setIs_read(Boolean is_read) { + this.is_read = is_read; + } + + public Boolean getIs_stock() { + return is_stock; + } + + public void setIs_stock(Boolean is_stock) { + this.is_stock = is_stock; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java new file mode 100644 index 0000000..93a7662 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/IssueEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; + +/** + * Created by thomas on 18.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Issue.findAll", query="SELECT i from IssueEntity as i"), + @NamedQuery(name="Issue.findByNumber", query="SELECT i from IssueEntity as i WHERE i.number = :number") +}) + +@Entity +@Table(name = "ISSUE") +public class IssueEntity { + + private Long id; + + private String number; + + private Boolean completed; + + private ComicEntity comic; + + private ArtistEntity writer; + + private ArtistEntity inker; + + private ArtistEntity penciler; + + private StoryArcEntity storyArc; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getNumber() { return number; } + + public void setNumber(String number) { this.number = number; } + + @Column + public Boolean getCompleted() { return completed; } + public Boolean isCompleted() { return completed; } + + public void setCompleted(Boolean completed) { this.completed = completed; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setWriter(ArtistEntity writer) { this.writer = writer; } + + @ManyToOne + public ArtistEntity getWriter() { return writer; } + + public void setInker(ArtistEntity inker) { this.inker = inker; } + + @ManyToOne + public ArtistEntity getInker() { return inker; } + + public void setPenciler(ArtistEntity penciler) { this.penciler = penciler; } + + @ManyToOne + public ArtistEntity getPenciler() { return penciler; } + + public void setStoryArc(StoryArcEntity storyArc) { this.storyArc = storyArc; } + + @ManyToOne + public StoryArcEntity getStoryArc() { return storyArc; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java new file mode 100644 index 0000000..e147d59 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Publisher.java @@ -0,0 +1,60 @@ +package com.ibtp.kontor.comics.entity; + +import com.ibtp.kontor.Database; +import org.bson.types.ObjectId; +import org.mongodb.morphia.Datastore; +import org.mongodb.morphia.annotations.Entity; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; + +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +@Entity("publisher") +public class Publisher { + + public Publisher() {} + + public Publisher(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public static Publisher getById(ObjectId publisherId) { + Datastore store = Database.init().getDatastore("comics"); + List results = store.createQuery(Publisher.class).field("id").equal(publisherId).asList(); + if (results.size() > 0) { + return results.get(0); + } else { + return null; + } + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java new file mode 100644 index 0000000..9aec40e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/PublisherEntity.java @@ -0,0 +1,47 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="Publisher.findAll", query="SELECT p from PublisherEntity as p"), + @NamedQuery(name="Publisher.findByName", query="SELECT p from PublisherEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name = "PUBLISHER") +public class PublisherEntity { + + private Long id; + + private String name; + + private Collection comic = new ArrayList(); + + public PublisherEntity() {} + + public PublisherEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + public void setComic(Collection comic) { this.comic = comic; } + + @OneToMany(mappedBy="publisher", cascade=CascadeType.REMOVE) + public Collection getComic() { return comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java new file mode 100644 index 0000000..4a4812b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArc.java @@ -0,0 +1,59 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; +import org.mongodb.morphia.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +public class StoryArc { + + @Id + private ObjectId id; + + @Property + private String name; + + @Reference + private Comic comic; + + @Reference + private List issues = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java new file mode 100644 index 0000000..e3ea22b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/StoryArcEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.comics.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by thomas on 17.01.15. + */ +@NamedQueries({ + @NamedQuery(name="StoryArc.findAll", query="SELECT s from StoryArcEntity as s"), + @NamedQuery(name="StoryArc.findByTitle", query="SELECT s from StoryArcEntity as s WHERE s.title = :title") +}) + +@Entity +@Table(name = "STORYARC") +public class StoryArcEntity { + + private Long id; + + private String title; + + private Collection issues = new ArrayList(); + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + public void setIssues(Collection issues) { this.issues = issues; } + + @OneToMany(mappedBy="storyArc", cascade=CascadeType.REMOVE) + public Collection getIssues() { return issues; } + + public void setComic(ComicEntity comic) { this.comic = comic; } + + @ManyToOne + public ComicEntity getComic() { return comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java b/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java new file mode 100644 index 0000000..161afd7 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/TradePaperback.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.comics.entity; + + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Reference; + +/** + * Created by thomas on 01.03.16. + */ +public class TradePaperback { + + @Id + private ObjectId id; + + @Reference + private Comic comic; + + @Reference + private String issue_start; + + @Reference + private String issue_end; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public String getIssue_start() { + return issue_start; + } + + public void setIssue_start(String issue_start) { + this.issue_start = issue_start; + } + + public String getIssue_end() { + return issue_end; + } + + public void setIssue_end(String issue_end) { + this.issue_end = issue_end; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java b/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java new file mode 100644 index 0000000..7191824 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/Volume.java @@ -0,0 +1,70 @@ +package com.ibtp.kontor.comics.entity; + +import org.bson.types.ObjectId; +import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Property; +import org.mongodb.morphia.annotations.Reference; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by thomas on 01.03.16. + */ +public class Volume { + + public Volume() {} + + public Volume(String name) { + setName(name); + } + + @Id + private ObjectId id; + + @Property + private String name; + + @Reference + private Comic comic; + + @Reference + private List issues = new ArrayList(); + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Comic getComic() { + return comic; + } + + public void setComic(Comic comic) { + this.comic = comic; + } + + public List getIssues() { + return issues; + } + + public void setIssues(List issues) { + this.issues = issues; + } + + @Override + public String toString() { + return name; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java b/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java new file mode 100644 index 0000000..6f896ad --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/entity/VolumeEntity.java @@ -0,0 +1,40 @@ +package com.ibtp.kontor.comics.entity; + + +import javax.persistence.*; + +/** + * Created by TPEETZ on 19.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Volume.findAll", query="SELECT v from VolumeEntity as v"), + @NamedQuery(name="Volume.findByTitle", query="SELECT v from VolumeEntity as v WHERE v.title = :title") +}) + +@Entity +@Table(name = "VOLUME") +public class VolumeEntity { + + private Long id; + + private String title; + + private ComicEntity comic; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } + + @ManyToOne + public ComicEntity getComic() { return comic; } + + public void setComic(ComicEntity comic) { this.comic = comic; } +} diff --git a/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java b/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java new file mode 100644 index 0000000..5201a11 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/comics/view/ComicsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.comics.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class ComicsMenu extends JMenu { + + public ComicsMenu() { + super("Comics"); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java b/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java new file mode 100644 index 0000000..fa22722 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/BaseImpl.java @@ -0,0 +1,21 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 16.01.2015. + */ +public class BaseImpl { + + protected BaseImpl() { + Logger logger = LoggerFactory.getLogger(this.getClass().getName()); + logger.info("BaseImpl started"); + } + + protected EntityManager getEntityManager() { + return DatabaseManager.getDatabase().getEntityManager(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/Database.java b/java/src/main/java/com/ibtp/kontor/dal/Database.java new file mode 100644 index 0000000..67dbda9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/Database.java @@ -0,0 +1,11 @@ +package com.ibtp.kontor.dal; + +import javax.persistence.EntityManager; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public interface Database { + + public EntityManager getEntityManager(); +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java b/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java new file mode 100644 index 0000000..0ccc25d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/DatabaseManager.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.dal; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 22.01.2015. + */ +public class DatabaseManager { + + private static Database database; + private static Logger logger = LoggerFactory.getLogger(DatabaseManager.class.getName()); + + public static Database getDatabase() { + logger.info("return " + database.toString()); + return database; + } + + public static void setDatabase(Database database) { + logger.info("set " + database.toString()); + DatabaseManager.database = database; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java b/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java new file mode 100644 index 0000000..e0ec9c1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/dal/LocalDatabase.java @@ -0,0 +1,104 @@ +package com.ibtp.kontor.dal; + +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class LocalDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + + private LocalDatabase() { + logger.info("LocalDatabase started"); + } + + private static void assureDatabaseRunning() { + if (LocalDatabase.server == null) { + LocalDatabase.startDatabase(); + } + } + + private static void startDatabase() { + Logger logger = LoggerFactory.getLogger(LocalDatabase.class.getName()); + logger.info("startDatabase as kontor in hsqldb_databases/kontor"); + LocalDatabase.server = new Server(); + LocalDatabase.server.setAddress("localhost"); + LocalDatabase.server.setDatabaseName(0, "kontor"); + LocalDatabase.server.setDatabasePath(0, "file:hsqldb_databases/kontor"); + LocalDatabase.server.setPort(2345); + LocalDatabase.server.setTrace(true); + LocalDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalDatabase.factory == null) { + LocalDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return LocalDatabase.factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalDatabase.server.toString(); + } + return LocalDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java new file mode 100644 index 0000000..f9cd144 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleDao.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface ArticleDao { + + public ArticleEntity getById(Long id); + + public List findByIds(List ids); + + public List findAll(); + + public List findByTitle(String title); + + public ArticleEntity store(ArticleEntity entity); + + public void delete(ArticleEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java new file mode 100644 index 0000000..ada92d9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/ArticleImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.ArticleEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImpl extends BaseImpl implements ArticleDao { + + @Override + public ArticleEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Article.findById"); + query.setParameter("id", id); + return (ArticleEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Article.findAll"); + //noinspection unchecked + return query.getResultList(); + } + + @Override + public List findByTitle(String title) { + Query query = getEntityManager().createNamedQuery("Article.findByTitle"); + query.setParameter("title", title); + //noinspection unchecked + return query.getResultList(); + } + + public ArticleEntity addArticle(String title) { + ArticleEntity entity = new ArticleEntity(title); + store(entity); + return entity; + } + + @Override + public ArticleEntity store(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ArticleEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java new file mode 100644 index 0000000..85f4c83 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorDao.java @@ -0,0 +1,25 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface AuthorDao { + + public AuthorEntity getById(Long id); + + public List findByIds(List ids); + + public List findByName(String name); + + public List findAll(); + + public AuthorEntity addAuthor(String name); + + public AuthorEntity store(AuthorEntity entity); + + public void delete(AuthorEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java new file mode 100644 index 0000000..573d7d9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/AuthorImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.AuthorEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImpl extends BaseImpl implements AuthorDao { + + public AuthorImpl() {} + + @Override + public AuthorEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Author.findById"); + query.setParameter("id", id); + return (AuthorEntity)query.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + return null; + } + + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Author.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public List findAll() { + Query query = getEntityManager().createNamedQuery("Author.findAll"); + return query.getResultList(); + } + + @Override + public AuthorEntity addAuthor(String name) { + AuthorEntity author = new AuthorEntity(name); + store(author); + return author; + } + + @Override + public AuthorEntity store(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(AuthorEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java new file mode 100644 index 0000000..d89a1b7 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/BookDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface BookDao { + + public BookEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BookEntity store(BookEntity entity); + + public void delete(BookEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java new file mode 100644 index 0000000..63f0ab1 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/BookImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.BookEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImpl extends BaseImpl implements BookDao { + + @Override + public BookEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public BookEntity store(BookEntity entity) { + return null; + } + + @Override + public void delete(BookEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java new file mode 100644 index 0000000..f123e69 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/FileDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface FileDao { + + public FileEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public FileEntity store(FileEntity entity); + + public void delete(FileEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java new file mode 100644 index 0000000..1af2900 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/FileImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.FileEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImpl extends BaseImpl implements FileDao { + + @Override + public FileEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public FileEntity store(FileEntity entity) { + return null; + } + + @Override + public void delete(FileEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java b/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java new file mode 100644 index 0000000..81da92b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/TitleDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +interface TitleDao { + + public TitleEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public TitleEntity store(TitleEntity entity); + + public void delete(TitleEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java b/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java new file mode 100644 index 0000000..410c2c5 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/dal/TitleImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.library.entity.TitleEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImpl extends BaseImpl implements TitleDao { + + @Override + public TitleEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public TitleEntity store(TitleEntity entity) { + return null; + } + + @Override + public void delete(TitleEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java new file mode 100644 index 0000000..d61eda5 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/ArticleEntity.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Article.findAll", query="SELECT a from ArticleEntity as a"), + @NamedQuery(name="Article.findByTitle", query="SELECT a from ArticleEntity as a WHERE a.title = :title") +}) + +@Entity +@Table(name = "ARTICLE") +public class ArticleEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + public ArticleEntity() {} + + public ArticleEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java new file mode 100644 index 0000000..63a6f4b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/AuthorEntity.java @@ -0,0 +1,62 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@NamedQueries({ + @NamedQuery(name="Author.findAll", query="SELECT a from AuthorEntity as a"), + @NamedQuery(name="Author.findById", query="SELECT a from AuthorEntity as a WHERE a.id = :id"), + @NamedQuery(name="Author.findByName", query="SELECT a from AuthorEntity as a WHERE a.name = :name") +}) +@Entity +@Table(name="AUTHOR") +public class AuthorEntity { + + private Long id; + + private String name; + + private Collection books = new ArrayList(); + + private Collection articles = new ArrayList(); + + public AuthorEntity() {} + + public AuthorEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getBooks() { + return books; + } + + public void setBooks(Collection books) { + this.books = books; + } + + @OneToMany(mappedBy="author", cascade=CascadeType.REMOVE) + public Collection getArticles() { + return articles; + } + + public void setArticles(Collection articles) { + this.articles = articles; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java new file mode 100644 index 0000000..b57705c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/BookEntity.java @@ -0,0 +1,96 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "BOOK") +public class BookEntity { + + private Long id; + + private String title; + + private AuthorEntity author; + + private String publisher; + + private String isbn; + + private Long page; + + private String edition; + + public BookEntity() {} + + public BookEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } + + @ManyToOne + public AuthorEntity getAuthor() { + return author; + } + + public void setAuthor(AuthorEntity author) { + this.author = author; + } + + @Column + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + @Column + public Long getPage() { + return page; + } + + public void setPage(Long page) { + this.page = page; + } + + @Column + public String getEdition() { + return edition; + } + + public void setEdition(String edition) { + this.edition = edition; + } + + @Column + public String getPublisher() { + return publisher; + } + + public void setPublisher(String publisher) { + this.publisher = publisher; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java new file mode 100644 index 0000000..05f4577 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/FileEntity.java @@ -0,0 +1,41 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "FILE") +public class FileEntity { + + private Long id; + + private String title; + + public FileEntity() {} + + public FileEntity(String title) { + setTitle(title); + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /* unused */ + public void setId(Long id) { + this.id = id; + } + + @Column + public String getTitle() { + return title; + } + + void setTitle(String title) { + this.title = title; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java b/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java new file mode 100644 index 0000000..ba25eb9 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/entity/TitleEntity.java @@ -0,0 +1,27 @@ +package com.ibtp.kontor.library.entity; + +import javax.persistence.*; + +/** + * Created by TPEETZ on 23.01.2015. + */ +@Entity +@Table(name = "TITLE") +public class TitleEntity { + + private Long id; + + private String title; + + @Id + @GeneratedValue(strategy= GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getTitle() { return title; } + + public void setTitle(String title) { this.title = title; } +} diff --git a/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java b/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java new file mode 100644 index 0000000..0b97e27 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/library/view/LibraryMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.library.view; + +import javax.swing.*; + +/** + * Created by tpeetz on 12.02.2015. + */ +public class LibraryMenu extends JMenu { + + public LibraryMenu() { + super("Library"); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java new file mode 100644 index 0000000..29692d4 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface BaseSetDao { + + public BaseSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public BaseSetEntity store(BaseSetEntity entity); + + public void delete(BaseSetEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java new file mode 100644 index 0000000..9e584a4 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/BaseSetImpl.java @@ -0,0 +1,51 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BaseSetImpl extends BaseImpl implements BaseSetDao { + + @Override + public BaseSetEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("BaseSet.findById"); + query.setParameter("id", id); + return (BaseSetEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("BaseSet.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public BaseSetEntity store(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(BaseSetEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java new file mode 100644 index 0000000..c68790f --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface InsertDao { + + public InsertEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public InsertEntity store(InsertEntity entity); + + public void delete(InsertEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java new file mode 100644 index 0000000..598763c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/InsertImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.InsertEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImpl extends BaseImpl implements InsertDao { + + @Override + public InsertEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public InsertEntity store(InsertEntity entity) { + return null; + } + + @Override + public void delete(InsertEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java new file mode 100644 index 0000000..451d18b --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerDao.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + + +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface ManufacturerDao { + + public ManufacturerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ManufacturerEntity assignBaseSet(ManufacturerEntity manufacturer, BaseSetEntity baseSet); + + public ManufacturerEntity addManufacturer(String name); + + public ManufacturerEntity store(ManufacturerEntity entity); + + public void delete(ManufacturerEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java new file mode 100644 index 0000000..58c2fd6 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.BaseSetEntity; +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class ManufacturerImpl extends BaseImpl implements ManufacturerDao { + + @Override + public ManufacturerEntity getById(Long id) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findById"); + q.setParameter("id", id); + return (ManufacturerEntity)q.getSingleResult(); + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query q = getEntityManager().createNamedQuery("Manufacturer.findByName"); + q.setParameter("name", name); + return q.getResultList(); + } + + @Override + public ManufacturerEntity assignBaseSet(ManufacturerEntity comic, BaseSetEntity baseSet) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public ManufacturerEntity addManufacturer(String name) { + ManufacturerEntity manufacturer = new ManufacturerEntity(name); + store(manufacturer); + return manufacturer; + } + + @Override + public ManufacturerEntity store(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(ManufacturerEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java new file mode 100644 index 0000000..c4d36ba --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface ParallelSetDao { + + public ParallelSetEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public ParallelSetEntity store(ParallelSetEntity entity); + + public void delete(ParallelSetEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java new file mode 100644 index 0000000..cb604a0 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.ParallelSetEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImpl extends BaseImpl implements ParallelSetDao { + + @Override + public ParallelSetEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public ParallelSetEntity store(ParallelSetEntity entity) { + return null; + } + + @Override + public void delete(ParallelSetEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java new file mode 100644 index 0000000..11d045e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerDao.java @@ -0,0 +1,24 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PlayerDao { + + public PlayerEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public PlayerEntity addPlayer(String name); + + public PlayerEntity store(PlayerEntity entity); + + public void delete(PlayerEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java new file mode 100644 index 0000000..c3d5104 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PlayerImpl.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PlayerEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImpl extends BaseImpl implements PlayerDao { + + @Override + public PlayerEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public PlayerEntity addPlayer(String name) { + return null; + } + + @Override + public PlayerEntity store(PlayerEntity entity) { + return null; + } + + @Override + public void delete(PlayerEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java new file mode 100644 index 0000000..9970491 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface PositionDao { + + public PositionEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public PositionEntity addPosition(String name); + + public PositionEntity store(PositionEntity entity); + + public void delete(PositionEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java new file mode 100644 index 0000000..e2a22ae --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/PositionImpl.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import java.util.Collection; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImpl extends BaseImpl implements PositionDao { + + @Override + public PositionEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Position.findById"); + query.setParameter("id", id); + return (PositionEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Position.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Position.findAll"); + return query.getResultList(); + } + + @Override + public PositionEntity addPosition(String name) { + PositionEntity position = new PositionEntity(name); + store(position); + return position; + } + + @Override + public PositionEntity store(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(PositionEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java new file mode 100644 index 0000000..25fb244 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardDao.java @@ -0,0 +1,22 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 27.01.2015. + */ +interface SportCardDao { + + public SportCardEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public SportCardEntity store(SportCardEntity entity); + + public void delete(SportCardEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java new file mode 100644 index 0000000..5939d2c --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportCardImpl.java @@ -0,0 +1,38 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportCardEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImpl extends BaseImpl implements SportCardDao { + + @Override + public SportCardEntity getById(Long id) { + return null; + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + return null; + } + + @Override + public SportCardEntity store(SportCardEntity entity) { + return null; + } + + @Override + public void delete(SportCardEntity entity) { + + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java new file mode 100644 index 0000000..f5ba51e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; + +/** + * + * @author tpeetz + */ +interface SportDao { + public SportEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public SportEntity addSport(String name); + + public SportEntity store(SportEntity entity); + + public void delete(SportEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java new file mode 100644 index 0000000..223236d --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/SportImpl.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import java.util.Collection; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.Query; + +/** + * + * @author tpeetz + */ +public class SportImpl extends BaseImpl implements SportDao { + + @Override + public SportEntity getById(Long id) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public List findByIds(List ids) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @SuppressWarnings("unchecked") + @Override + public List findByName(String name) { + Query query = getEntityManager().createNamedQuery("Sport.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Sport.findAll"); + return query.getResultList(); + } + + @Override + public SportEntity addSport(String name) { + SportEntity sport = new SportEntity(name); + store(sport); + return sport; + } + + @Override + public SportEntity store(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(SportEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java new file mode 100644 index 0000000..c2f9eff --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamDao.java @@ -0,0 +1,26 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +interface TeamDao { + public TeamEntity getById(Long id); + + public Collection findByIds(List ids); + + public Collection findByName(String name); + + public Collection findAll(); + + public TeamEntity addTeam(String name, SportEntity sport); + + public TeamEntity store(TeamEntity entity); + + public void delete(TeamEntity entity); +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java new file mode 100644 index 0000000..7fb8244 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/dal/TeamImpl.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.BaseImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class TeamImpl extends BaseImpl implements TeamDao { + + @Override + public TeamEntity getById(Long id) { + Query query = getEntityManager().createNamedQuery("Team.findById"); + query.setParameter("id", id); + return (TeamEntity)query.getSingleResult(); + } + + @Override + public Collection findByIds(List ids) { + return null; + } + + @Override + public Collection findByName(String name) { + Query query = getEntityManager().createNamedQuery("Team.findByName"); + query.setParameter("name", name); + return query.getResultList(); + } + + @Override + public Collection findAll() { + Query query = getEntityManager().createNamedQuery("Team.findAll"); + return query.getResultList(); + } + + @Override + public TeamEntity addTeam(String name, SportEntity sport) { + TeamEntity team = new TeamEntity(name); + team.setSport(sport); + store(team); + return team; + } + + @Override + public TeamEntity store(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.persist(entity); + em.getTransaction().commit(); + return entity; + } + + @Override + public void delete(TeamEntity entity) { + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + em.remove(entity); + em.getTransaction().commit(); + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java new file mode 100644 index 0000000..3111e98 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/BaseSetEntity.java @@ -0,0 +1,67 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="BASESET") +public class BaseSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="baseSet", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java new file mode 100644 index 0000000..ce59037 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/InsertEntity.java @@ -0,0 +1,74 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="InsertSet.findAll", query="SELECT i from InsertEntity as i"), + @NamedQuery(name="InsertSet.findById", query="SELECT i from InsertEntity as i WHERE i.id = :id"), + @NamedQuery(name="InsertSet.findByName", query="SELECT i from InsertEntity as i WHERE i.name = :name") +}) + +@Entity +@Table(name="INSERTSET") +public class InsertEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + private BaseSetEntity baseSet; + private Collection sportCard = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @OneToMany(mappedBy="insert", cascade=CascadeType.REMOVE) + public Collection getSportCard() { + return sportCard; + } + + public void setSportCard(Collection sportCard) { + this.sportCard = sportCard; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java new file mode 100644 index 0000000..f83da2a --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ManufacturerEntity.java @@ -0,0 +1,78 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Manufacturer.findAll", query="SELECT m from ManufacturerEntity as m"), + @NamedQuery(name="Manufacturer.findByName", query="SELECT m from ManufacturerEntity as m WHERE m.name = :name") +}) + +@Entity +@Table(name="MANUFACTURER") +public class ManufacturerEntity { + + private Long id; + private String name; + private Collection baseSets = new ArrayList(); + private Collection parallelSets = new ArrayList(); + private Collection inserts = new ArrayList(); + + public ManufacturerEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getBaseSets() { + return baseSets; + } + + public void setBaseSets(Collection baseSets) { + this.baseSets = baseSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getParallelSets() { + return parallelSets; + } + + public void setParallelSets(Collection parallelSets) { + this.parallelSets = parallelSets; + } + + @OneToMany(mappedBy="manufacturer", cascade=CascadeType.REMOVE) + public Collection getInserts() { + return inserts; + } + + public void setInserts(Collection inserts) { + this.inserts = inserts; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java new file mode 100644 index 0000000..0ce0c69 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/ParallelSetEntity.java @@ -0,0 +1,53 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name="PARALLELSET") +public class ParallelSetEntity { + private Long id; + private String name; + private ManufacturerEntity manufacturer; + + private BaseSetEntity baseSet; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + @ManyToOne + public ManufacturerEntity getManufacturer() { + return manufacturer; + } + + public void setManufacturer(ManufacturerEntity manufacturer) { + this.manufacturer = manufacturer; + } + + public void setName(String name) { + this.name = name; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java new file mode 100644 index 0000000..7c19d70 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PlayerEntity.java @@ -0,0 +1,46 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@Entity +@Table(name="PLAYER") +public class PlayerEntity { + private Long id; + private TeamEntity team; + private Collection cards = new ArrayList(); + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public TeamEntity getTeam() { + return team; + } + + public void setTeam(TeamEntity team) { + this.team = team; + } + + @OneToMany(mappedBy="player", cascade=CascadeType.REMOVE) + public Collection getCards() { + return cards; + } + + public void setCards(Collection cards) { + this.cards = cards; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java new file mode 100644 index 0000000..0e5c6b8 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/PositionEntity.java @@ -0,0 +1,66 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQuery; +import javax.persistence.NamedQueries; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Position.findAll", query="SELECT p from PositionEntity as p"), + @NamedQuery(name="Position.findByName", query="SELECT p from PositionEntity as p WHERE p.name = :name") +}) + +@Entity +@Table(name="POSITION") +public class PositionEntity { + + private Long id; + private String name; + private String shortName; + private SportEntity sport; + + public PositionEntity() {} + + public PositionEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Column + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + @ManyToOne + public SportEntity getSport() { + return sport; + } + + public void setSport(SportEntity sport) { + this.sport = sport; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java new file mode 100644 index 0000000..f71f722 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportCardEntity.java @@ -0,0 +1,68 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="SportCard.findAll", query="SELECT s from SportCardEntity as s") +}) + +@Entity +@Table(name = "SPORTCARD") +public class SportCardEntity { + private Long id; + private PlayerEntity player; + private BaseSetEntity baseSet; + private ParallelSetEntity parallelSet; + private InsertEntity insert; + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @ManyToOne + public PlayerEntity getPlayer() { + return player; + } + + public void setPlayer(PlayerEntity player) { + this.player = player; + } + + @ManyToOne + public BaseSetEntity getBaseSet() { + return baseSet; + } + + public void setBaseSet(BaseSetEntity baseSet) { + this.baseSet = baseSet; + } + + @ManyToOne + public ParallelSetEntity getParallelSet() { + return parallelSet; + } + + public void setParallelSet(ParallelSetEntity parallelSet) { + this.parallelSet = parallelSet; + } + + @ManyToOne + public InsertEntity getInsert() { + return insert; + } + + public void setInsert(InsertEntity insert) { + this.insert = insert; + } + +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java new file mode 100644 index 0000000..8e2258e --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/SportEntity.java @@ -0,0 +1,75 @@ +package com.ibtp.kontor.tradingcards.entity; + +import java.util.ArrayList; +import java.util.Collection; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Sport.findAll", query="SELECT s from SportEntity as s"), + @NamedQuery(name="Sport.findByName", query="SELECT s from SportEntity as s WHERE s.name = :name") +}) + +@Entity +@Table(name="SPORT") +public class SportEntity { + + private Long id; + private String name; + private Collection teams = new ArrayList(); + private Collection positions = new ArrayList(); + + public SportEntity() {} + + public SportEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + String getName() { + return name; + } + + void setName(String name) { + this.name = name; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getTeams() { + return teams; + } + + public void setTeams(Collection teams) { + this.teams = teams; + } + + @OneToMany(mappedBy="sport", cascade=CascadeType.REMOVE) + public Collection getPositions() { + return positions; + } + + public void setPositions(Collection positions) { + this.positions = positions; + } + + @Override + public String toString() { + return "Sport[" + "id=" + getId() + ",name=" + getName() + "]"; + } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java new file mode 100644 index 0000000..8dbcb77 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/entity/TeamEntity.java @@ -0,0 +1,48 @@ +package com.ibtp.kontor.tradingcards.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +@NamedQueries({ + @NamedQuery(name="Team.findAll", query="SELECT t from TeamEntity as t"), + @NamedQuery(name="Team.findByName", query="SELECT t from TeamEntity as t WHERE t.name = :name") +}) + +@Entity +@Table(name="TEAM") +public class TeamEntity { + + private Long id; + private String name; + private SportEntity sport; + + public TeamEntity() {} + + public TeamEntity(String name) { + setName(name); + } + + @Id + @GeneratedValue(strategy=GenerationType.AUTO) + public Long getId() { return id; } + + @SuppressWarnings("unused") + private void setId(Long id) { this.id = id; } + + @Column + public String getName() { return name; } + + void setName(String name) { this.name = name; } + + @ManyToOne + public SportEntity getSport() { return sport; } + + public void setSport(SportEntity sport) { this.sport = sport; } +} diff --git a/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java b/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java new file mode 100644 index 0000000..f124056 --- /dev/null +++ b/java/src/main/java/com/ibtp/kontor/tradingcards/view/TradingCardsMenu.java @@ -0,0 +1,13 @@ +package com.ibtp.kontor.tradingcards.view; + +import javax.swing.*; + +/** + * Created by TPEETZ on 13.02.2015. + */ +public class TradingCardsMenu extends JMenu { + + public TradingCardsMenu() { + super("TradingCards"); + } +} diff --git a/java/src/main/resources/META-INF/persistence.xml b/java/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java/src/main/resources/logback.xml b/java/src/main/resources/logback.xml new file mode 100644 index 0000000..e8cff02 --- /dev/null +++ b/java/src/main/resources/logback.xml @@ -0,0 +1,40 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + c:/kontor.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + c:/kontor.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + diff --git a/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java b/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java new file mode 100644 index 0000000..57d01a3 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/CollectionTest.java @@ -0,0 +1,43 @@ +package com.ibtp.kontor.comics; + +import com.ibtp.kontor.comics.dal.PublisherImpl; +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterAll + public static void cleanup() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherEntities = publisherImpl.findAll(); + for (PublisherEntity publisherEntity : publisherEntities) { + publisherImpl.delete(publisherEntity); + } + } + + @Test + public void testAddPublishers() { + String publisherName = "Bongo Comics"; + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher(publisherName); + publisherImpl.addPublisher("Marvel"); + Collection publisherList = publisherImpl.findAll(); + assertEquals(2, publisherList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java new file mode 100644 index 0000000..e78e77f --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/ArtistImplTest.java @@ -0,0 +1,65 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ArtistEntity; +import com.ibtp.kontor.dal.*; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collection; + +public class ArtistImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + tearDown(); + } + + @AfterAll + public static void tearDown() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + for (ArtistEntity artistEntity : artistList) { + artistImpl.delete(artistEntity); + } + } + + @Test + public void testArtistAddAndDelete() { + String artistName = "testArtistAddAndDelete"; + ArtistImpl artistImpl = new ArtistImpl(); + artistImpl.addArtist(artistName); + Collection resultList = artistImpl.findByName(artistName); + assertNotNull(resultList); + assertTrue(resultList.size() > 0); + ArtistEntity artist = (ArtistEntity)(resultList.toArray()[0]); + artistImpl.delete(artist); + resultList = artistImpl.findByName(artistName); + assertNotNull(resultList); + assertEquals(0, resultList.size()); + } + + @Test + public void testArtistFindAll() { + ArtistImpl artistImpl = new ArtistImpl(); + Collection artistList = artistImpl.findAll(); + assertNotNull(artistList); + assertEquals(0, artistList.size()); + artistImpl.addArtist("testArtistFindAll1"); + artistImpl.addArtist("testArtistFindAll2"); + artistImpl.addArtist("testArtistFindAll3"); + artistList = artistImpl.findAll(); + assertNotNull(artistList); + assertEquals(3, artistList.size()); + for (ArtistEntity artistEntity : artistList) { + artistImpl.delete(artistEntity); + } + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java new file mode 100644 index 0000000..76f2d00 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/ComicImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.ComicEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class ComicImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testComicAddAndDelete() { + String comicTitle = "Comic1"; + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic(comicTitle); + Collection comicList = comicImpl.findByTitle(comicTitle); + assertNotNull(comicList); + assertEquals(1, comicList.size()); + comicImpl.delete((ComicEntity) comicList.toArray()[0]); + comicList = comicImpl.findByTitle(comicTitle); + assertNotNull(comicList); + assertEquals(0, comicList.size()); + } + + @Test + public void testComicFindAll() { + ComicImpl comicImpl = new ComicImpl(); + comicImpl.addComic("Comic1"); + comicImpl.addComic("Comic2"); + comicImpl.addComic("Comic3"); + Collection comicList = comicImpl.findAll(); + assertNotNull(comicList); + assertEquals(3, comicList.size()); + for (Iterator iterator = comicList.iterator(); iterator.hasNext(); ) { + ComicEntity next = iterator.next(); + comicImpl.delete(next); + } + comicList = comicImpl.findAll(); + assertNotNull(comicList); + assertEquals(0, comicList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java new file mode 100644 index 0000000..5797305 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/IssueImplTest.java @@ -0,0 +1,64 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.IssueEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class IssueImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testIssueAddAndDelete() { + String issueNumber = "42"; + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue = new IssueEntity(); + issue.setNumber(issueNumber); + issueImpl.store(issue); + Collection issueList = issueImpl.findByNumber(issueNumber); + assertNotNull(issueList); + assertEquals(1, issueList.size()); + issueImpl.delete(issue); + issueList = issueImpl.findByNumber(issueNumber); + assertNotNull(issueList); + assertEquals(0, issueList.size()); + } + + @Test + public void testIssueFindAll() { + IssueImpl issueImpl = new IssueImpl(); + IssueEntity issue1 = new IssueEntity(); + issue1.setNumber("issue1"); + IssueEntity issue2 = new IssueEntity(); + issue1.setNumber("issue2"); + IssueEntity issue3 = new IssueEntity(); + issue1.setNumber("issue3"); + issueImpl.store(issue1); + issueImpl.store(issue2); + issueImpl.store(issue3); + Collection issueList = issueImpl.findAll(); + assertNotNull(issueList); + assertEquals(3, issueList.size()); + for (IssueEntity issueEntity : issueList) { + issueImpl.delete(issueEntity); + } + issueList = issueImpl.findAll(); + assertNotNull(issueList); + assertEquals(0, issueList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java new file mode 100644 index 0000000..b795e96 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/PublisherImplTest.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.PublisherEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 20.01.2015. + */ +public class PublisherImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterEach + public void cleanUp() { + PublisherImpl publisherImpl = new PublisherImpl(); + Collection publisherList = publisherImpl.findAll(); + for (PublisherEntity publisherEntity : publisherList) { + publisherImpl.delete(publisherEntity); + } + } + + @Test + public void testPublisherAddAndDelete() { + String publisherName = "testPublisherAddAndDelete"; + PublisherImpl publisherImpl = new PublisherImpl(); + PublisherEntity publisher = publisherImpl.addPublisher(publisherName); + Collection publisherList = publisherImpl.findByName(publisherName); + assertEquals(1, publisherList.size()); + publisherImpl.delete(publisher); + publisherList = publisherImpl.findByName(publisherName); + assertEquals(0, publisherList.size()); + } + + @Test + public void testPublisherFindAll() { + PublisherImpl publisherImpl = new PublisherImpl(); + publisherImpl.addPublisher("testDeletePublisher1"); + publisherImpl.addPublisher("testDeletePublisher2"); + publisherImpl.addPublisher("testDeletePublisher3"); + Collection publisherList = publisherImpl.findAll(); + assertEquals(3, publisherList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java new file mode 100644 index 0000000..2768e32 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/StoryArcImplTest.java @@ -0,0 +1,63 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.StoryArcEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class StoryArcImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + StoryArcImpl storyArcImpl = new StoryArcImpl(); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + for (StoryArcEntity storyArcEntity : storyArcEntityCollection) { + storyArcImpl.delete(storyArcEntity); + } + } + + @Test + public void testStoryArcAddAndDelete() { + String storyArcTtitle = "testStoryArcAddAndDelete"; + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc = new StoryArcEntity(); + storyArc.setTitle(storyArcTtitle); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + assertNotNull(storyArcEntityCollection); + assertEquals(1, storyArcEntityCollection.size()); + storyArcImpl.delete(storyArc); + storyArcEntityCollection = storyArcImpl.findByTitle(storyArcTtitle); + assertNotNull(storyArcEntityCollection); + assertEquals(0, storyArcEntityCollection.size()); + } + + @Test + public void testStoryArcFindAll() { + StoryArcImpl storyArcImpl = new StoryArcImpl(); + StoryArcEntity storyArc; + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll1"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll2"); + storyArcImpl.store(storyArc); + storyArc = new StoryArcEntity(); + storyArc.setTitle("testStoryArcFindAll3"); + storyArcImpl.store(storyArc); + Collection storyArcEntityCollection = storyArcImpl.findAll(); + assertNotNull(storyArcEntityCollection); + assertEquals(3, storyArcEntityCollection.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java b/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java new file mode 100644 index 0000000..fd34aa4 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/comics/dal/VolumeImplTest.java @@ -0,0 +1,72 @@ +package com.ibtp.kontor.comics.dal; + +import com.ibtp.kontor.comics.entity.VolumeEntity; +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 28.01.2015. + */ +public class VolumeImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @AfterEach + public void cleanUp() { + VolumeImpl volumeImpl = new VolumeImpl(); + Collection volumeList = volumeImpl.findAll(); + for (VolumeEntity volumeEntity : volumeList) { + volumeImpl.delete(volumeEntity); + } + } + + @Test + public void testVolumeAddAndDelete() { + String volumeTitle = "testVolumeAddAndDelete"; + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume = new VolumeEntity(); + volume.setTitle(volumeTitle); + VolumeEntity volumeEntity = volumeImpl.store(volume); + assertNotNull(volumeEntity); + assertEquals(volumeTitle, volumeEntity.getTitle()); + Collection volumeList = volumeImpl.findByTitle(volumeTitle); + assertNotNull(volumeList); + assertEquals(1, volumeList.size()); + VolumeEntity result = (VolumeEntity)volumeList.toArray()[0]; + assertEquals(volume, result); + volumeImpl.delete(result); + volumeList = volumeImpl.findByTitle(volumeTitle); + assertEquals(0, volumeList.size()); + } + + @Test + public void testVolumeFindAll() { + VolumeImpl volumeImpl = new VolumeImpl(); + VolumeEntity volume; + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll1"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll2"); + volumeImpl.store(volume); + volume = new VolumeEntity(); + volume.setTitle("testVolumeFindAll3"); + volumeImpl.store(volume); + Collection volumeList = volumeImpl.findAll(); + assertNotNull(volumeList); + assertEquals(3, volumeList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java b/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java new file mode 100644 index 0000000..a9737ac --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/dal/DataAccessLayerTest.java @@ -0,0 +1,63 @@ +package com.ibtp.kontor.dal; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 10.02.2015. + */ +public class DataAccessLayerTest { + + public void findTests(String packageName, String entityName) { + String testClassName = packageName + entityName + "ImplTest"; + Class testClass; + try { + testClass = Class.forName(testClassName); + Method addAndDelete = testClass.getMethod("test" + entityName + "AddAndDelete"); + Method findAll = testClass.getMethod("test" + entityName + "FindAll"); + } catch (ClassNotFoundException e) { + fail("Class " + testClassName + " missing"); + } catch (NoSuchMethodException e) { + fail("Test method for class " + testClassName + " missing"); + } + } + + @Test + public void testFindComicTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Artist", "Comic", "Issue", "Publisher", "StoryArc", "Volume"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.comics.dal.", testEntity); + } + } + + @Test + public void testFindLibraryTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"Article", "Author", "Book", "File", "Title"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.library.dal.", testEntity); + } + } + + @Test + public void testFindTradingCardsTests() { + /* + * Find all Tests + */ + String[] testClasses = new String[]{"BaseSet", "Insert", "Manufacturer", "ParallelSet", "Player", "Position", "SportCard", "Sport", "Team"}; + for (int i = 0; i < testClasses.length; i++) { + String testEntity = testClasses[i]; + findTests("com.ibtp.kontor.tradingcards.dal." , testEntity); + } + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java b/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java new file mode 100644 index 0000000..f6609fb --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/BookshelfTest.java @@ -0,0 +1,23 @@ +package com.ibtp.kontor.library; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookshelfTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthors() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java new file mode 100644 index 0000000..3fc8772 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/ArticleImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.library.entity.ArticleEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Collection; +import java.util.List; + +/** + * Created by tpeetz on 23.01.2015. + */ +public class ArticleImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + cleanUp(); + } + + @AfterAll + public static void cleanUp() { + ArticleImpl articleImpl = new ArticleImpl(); + Collection articleList = articleImpl.findAll(); + for (ArticleEntity articleEntity : articleList) { + articleImpl.delete(articleEntity); + } + } + + @Test + public void testAddArticle() { + String articleTitle = "testAddArticle"; + ArticleImpl articleImpl = new ArticleImpl(); + ArticleEntity article = articleImpl.addArticle(articleTitle); + assertNotNull(article); + List articleList = articleImpl.findByTitle(articleTitle); + assertEquals(1, articleList.size()); + } + + @Test + public void testArticleAddAndDelete() { + + } + + @Test + public void testArticleFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java new file mode 100644 index 0000000..cec2048 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/AuthorImplTest.java @@ -0,0 +1,56 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.library.entity.AuthorEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +/** + * Created by thomas on 23.01.15. + */ +public class AuthorImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testAddAuthor() { + String authorName = "testAddAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + assertNotNull(author); + } + + @Test + public void testDeleteAuthor() { + String authorName = "testDeleteAuthor"; + AuthorImpl authorImpl = new AuthorImpl(); + AuthorEntity author = authorImpl.addAuthor(authorName); + assertNotNull(author); + List authorList = authorImpl.findByName(authorName); + assertEquals(1, authorList.size()); + authorImpl.delete(author); + authorList = authorImpl.findByName(authorName); + assertEquals(0, authorList.size()); + } + + @Test + public void testAuthorAddAndDelete() { + + } + + @Test + public void testAuthorFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java new file mode 100644 index 0000000..95080e8 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/BookImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class BookImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBookAddAndDelete() { + + } + + @Test + public void testBookFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java new file mode 100644 index 0000000..dd0c690 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/FileImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class FileImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testFileAddAndDelete() { + + } + + @Test + public void testFileFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java b/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java new file mode 100644 index 0000000..92acd55 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/library/dal/TitleImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.library.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class TitleImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTitleAddAndDelete() { + + } + + @Test + public void testTitleFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java new file mode 100644 index 0000000..69e28ad --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/CollectionTest.java @@ -0,0 +1,168 @@ +package com.ibtp.kontor.tradingcards; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.tradingcards.dal.TeamImpl; +import com.ibtp.kontor.tradingcards.entity.TeamEntity; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.dal.SportImpl; +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.Iterator; + +/** + * Created by TPEETZ on 27.01.2015. + */ +public class CollectionTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + setupSports(); + } + + public static void setupSports() { + SportImpl sportImpl = new SportImpl(); + SportEntity football = sportImpl.addSport("Football"); + setupFootballTeams(football); + SportEntity baseball = sportImpl.addSport("Baseball"); + setupFootballTeams(baseball); + SportEntity basketball = sportImpl.addSport("Basketball"); + setupBasketballTeams(basketball); + SportEntity hockey = sportImpl.addSport("Hockey"); + setupHockeyTeams(hockey); + } + + public static void setupFootballTeams(SportEntity football) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Dallas Cowboys", football); + teamImpl.addTeam("New York Giants", football); + teamImpl.addTeam("Philadelphia Eagles", football); + teamImpl.addTeam("Arizona Cardinals", football); + teamImpl.addTeam("Washington Redskins", football); + teamImpl.addTeam("Detroit Lions", football); + teamImpl.addTeam("Minnesota Vikings", football); + teamImpl.addTeam("Green Bay Packers", football); + teamImpl.addTeam("Chicago Bears", football); + teamImpl.addTeam("Tampa Bay Buccaneers", football); + teamImpl.addTeam("San Francisco 49ers", football); + teamImpl.addTeam("New Orleans Saints", football); + teamImpl.addTeam("Atlanta Falcons", football); + teamImpl.addTeam("Los Angeles Rams", football); + teamImpl.addTeam("Buffalo Bills", football); + teamImpl.addTeam("Miami Dolphins", football); + teamImpl.addTeam("New York Jets", football); + teamImpl.addTeam("New England Patriots", football); + teamImpl.addTeam("Indianapolis Colts", football); + teamImpl.addTeam("Houston Oilers", football); + teamImpl.addTeam("Pittsburgh Steelers", football); + teamImpl.addTeam("Cleveland Browns", football); + teamImpl.addTeam("Kansas City Chiefs", football); + teamImpl.addTeam("Los Angeles Raiders", football); + teamImpl.addTeam("Denver Broncos", football); + teamImpl.addTeam("San Diego Chargers", football); + teamImpl.addTeam("Seattle Seahawks", football); + teamImpl.addTeam("Jacksonville Jaguars", football); + teamImpl.addTeam("Houston Texans", football); + } + + public static void setupBaseballTeams(SportEntity baseball) { + TeamImpl teamImpl = new TeamImpl(); + } + + public static void setupBasketballTeams(SportEntity basketball) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("Houston Rockets", basketball); + teamImpl.addTeam("San Antonio Spurs", basketball); + teamImpl.addTeam("Utah Jazz", basketball); + teamImpl.addTeam("Denver Nuggets", basketball); + teamImpl.addTeam("Minnesota Timberwolves", basketball); + teamImpl.addTeam("Dallas Mavericks", basketball); + teamImpl.addTeam("Seattle SuperSonics", basketball); + teamImpl.addTeam("Phoenix Suns", basketball); + teamImpl.addTeam("Golden State Warriors", basketball); + teamImpl.addTeam("Portland Trail Blazers", basketball); + teamImpl.addTeam("Los Angeles Lakers", basketball); + teamImpl.addTeam("Sacramento Kings", basketball); + teamImpl.addTeam("Los Angeles Clippers", basketball); + teamImpl.addTeam("New York Knicks", basketball); + teamImpl.addTeam("Orlando Magic", basketball); + teamImpl.addTeam("New Jersey Nets", basketball); + teamImpl.addTeam("Miami Heat", basketball); + teamImpl.addTeam("Boston Celtics", basketball); + teamImpl.addTeam("Philadelphia 76ers", basketball); + teamImpl.addTeam("Washington Bullets", basketball); + teamImpl.addTeam("Atlanta Hawks", basketball); + teamImpl.addTeam("Chicago Bulls", basketball); + teamImpl.addTeam("Indiana Pacers", basketball); + teamImpl.addTeam("Cleveland Cavaliers", basketball); + teamImpl.addTeam("Charlotte Hornets", basketball); + teamImpl.addTeam("Detroit Pistons", basketball); + teamImpl.addTeam("Milwaukee Bucks", basketball); + } + + public static void setupHockeyTeams(SportEntity hockey) { + TeamImpl teamImpl = new TeamImpl(); + teamImpl.addTeam("New York Rangers", hockey); + teamImpl.addTeam("Buffalo Sabers", hockey); + teamImpl.addTeam("Detroit Red Wings", hockey); + teamImpl.addTeam("Vancouver Canucks", hockey); + teamImpl.addTeam("Mighty Ducks of Anaheim", hockey); + teamImpl.addTeam("Calgary Flames", hockey); + teamImpl.addTeam("Edmonton Oilers", hockey); + teamImpl.addTeam("Los Angeles Kings", hockey); + teamImpl.addTeam("San Jose Sharks", hockey); + teamImpl.addTeam("Chicago Blackhawks", hockey); + teamImpl.addTeam("Dallas Stars", hockey); + teamImpl.addTeam("St. Louis Blues", hockey); + teamImpl.addTeam("Toronto Maple Leafs", hockey); + teamImpl.addTeam("Winnipeg Jets", hockey); + teamImpl.addTeam("Boston Bruins", hockey); + teamImpl.addTeam("Hartford Whalers", hockey); + teamImpl.addTeam("Montreal Canadiers", hockey); + teamImpl.addTeam("Ottawa Senators", hockey); + teamImpl.addTeam("Pittsburgh Penguins", hockey); + teamImpl.addTeam("Quebec Nordiques", hockey); + teamImpl.addTeam("Florida Panthers", hockey); + teamImpl.addTeam("New Jersey Devils", hockey); + teamImpl.addTeam("New York Islanders", hockey); + teamImpl.addTeam("Philadelphia Flyers", hockey); + teamImpl.addTeam("Tamba Bay Lightning", hockey); + teamImpl.addTeam("Washington Capitals", hockey); + } + + @AfterAll + public static void tearDown() { + TeamImpl teamImpl = new TeamImpl(); + Collection teamEntities = teamImpl.findAll(); + for (TeamEntity teamEntity : teamEntities) { + teamImpl.delete(teamEntity); + } + SportImpl sportImpl = new SportImpl(); + Collection sportEntities = sportImpl.findAll(); + for (SportEntity sportEntity : sportEntities) { + sportImpl.delete(sportEntity); + } + } + + @Test + public void gettAllSports() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + assertEquals(4, resultList.size()); + } + + @Test + public void getAllTeams() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + assertEquals(111, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java new file mode 100644 index 0000000..9f3bb4c --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/BaseSetImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class BaseSetImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testBaseSetAddAndDelete() { + + } + + @Test + public void testBaseSetFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java new file mode 100644 index 0000000..c246743 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/InsertImplTest.java @@ -0,0 +1,29 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class InsertImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + + @Test + public void testInsertAddAndDelete() { + + } + + @Test + public void testInsertFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java new file mode 100644 index 0000000..3681a3f --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ManufacturerImplTest.java @@ -0,0 +1,57 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.ManufacturerEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class ManufacturerImplTest { + + @BeforeAll + public static void setup() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void addManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + ManufacturerEntity manufacturer = manufacturerImpl.addManufacturer(manufacturerName); + assertNotNull(manufacturer); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertTrue(manufacturerList.size() > 0); + } + + @Test + public void deleteManufacturer() { + String manufacturerName = "Manufacturer1"; + ManufacturerImpl manufacturerImpl = new ManufacturerImpl(); + List manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertTrue(manufacturerList.size() > 0); + manufacturerImpl.delete(manufacturerList.get(0)); + manufacturerList = manufacturerImpl.findByName(manufacturerName); + assertEquals(0, manufacturerList.size()); + } + + @Test + public void testManufacturerAddAndDelete() { + + } + + @Test + public void testManufacturerFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java new file mode 100644 index 0000000..b674dd1 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/ParallelSetImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class ParallelSetImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testParallelSetAddAndDelete() { + + } + + @Test + public void testParallelSetFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java new file mode 100644 index 0000000..bee6239 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PlayerImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PlayerImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPlayerAddAndDelete() { + + } + + @Test + public void testPlayerFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java new file mode 100644 index 0000000..9271707 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/PositionImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.PositionEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class PositionImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testPositionAddAndDelete() { + String positionName = "testPositionAddAndDelete"; + PositionImpl positionImpl = new PositionImpl(); + PositionEntity position = positionImpl.addPosition(positionName); + Collection resultList = positionImpl.findByName(positionName); + assertEquals(1, resultList.size()); + positionImpl.delete(position); + resultList = positionImpl.findByName(positionName); + assertEquals(0, resultList.size()); + } + + @Test + public void testPositionFindAll() { + PositionImpl positionImpl = new PositionImpl(); + Collection resultList = positionImpl.findAll(); + assertEquals(0, resultList.size()); + } +} + diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java new file mode 100644 index 0000000..6cc8b5c --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportCardImplTest.java @@ -0,0 +1,28 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Created by tpeetz on 27.01.2015. + */ +public class SportCardImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportCardAddAndDelete() { + + } + + @Test + public void testSportCardFindAll() { + + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java new file mode 100644 index 0000000..6a4d580 --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/SportImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.SportEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; +import java.util.List; + +/** + * Created by TPEETZ on 19.01.2015. + */ +public class SportImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testSportAddAndDelete() { + String sportName = "testSportAddAndDelete"; + SportImpl sportImpl = new SportImpl(); + SportEntity sport = sportImpl.addSport(sportName); + List sportList = sportImpl.findByName(sportName); + assertEquals(1, sportList.size()); + sportImpl.delete(sport); + List result = sportImpl.findByName(sportName); + assertEquals(0, result.size()); + } + + @Test + public void testSportFindAll() { + SportImpl sportImpl = new SportImpl(); + Collection resultList = sportImpl.findAll(); + assertEquals(0, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java new file mode 100644 index 0000000..7e94a1b --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/tradingcards/dal/TeamImplTest.java @@ -0,0 +1,44 @@ +package com.ibtp.kontor.tradingcards.dal; + +import com.ibtp.kontor.dal.DatabaseManager; +import com.ibtp.kontor.util.LocalTestDatabase; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.ibtp.kontor.tradingcards.entity.TeamEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +/** + * Created by tpeetz on 20.01.2015. + */ +public class TeamImplTest { + + @BeforeAll + public static void setUp() { + DatabaseManager.setDatabase(new LocalTestDatabase()); + } + + @Test + public void testTeamAddAndDelete() { + String teamName = "testTeamAddAndDelete"; + TeamEntity team = new TeamEntity(teamName); + TeamImpl teamImpl = new TeamImpl(); + teamImpl.store(team); + Collection resultList = teamImpl.findByName(teamName); + assertEquals(1, resultList.size()); + teamImpl.delete(team); + resultList = teamImpl.findByName(teamName); + assertEquals(0, resultList.size()); + } + + @Test + public void testTeamFindAll() { + TeamImpl teamImpl = new TeamImpl(); + Collection resultList = teamImpl.findAll(); + assertEquals(0, resultList.size()); + } +} diff --git a/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java b/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java new file mode 100644 index 0000000..503104a --- /dev/null +++ b/java/src/test/java/com/ibtp/kontor/util/LocalTestDatabase.java @@ -0,0 +1,110 @@ +package com.ibtp.kontor.util; + +import com.ibtp.kontor.dal.Database; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.hsqldb.Server; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Created by TPEETZ on 21.01.2015. + */ +public class LocalTestDatabase implements Database { + + private static Server server; + private static EntityManagerFactory factory; + private static EntityManager em; + private static Logger logger = LoggerFactory.getLogger(LocalTestDatabase.class.getName()); + + static { + logger.info("initialization and starting database"); + LocalTestDatabase.assureDatabaseRunning(); + } + + public LocalTestDatabase() { + logger.info("LocalDatabaseTest started"); + } + + private static void assureDatabaseRunning() { + if (LocalTestDatabase.server == null) { + LocalTestDatabase.startDatabase(); + } + } + + private static void startDatabase() { + logger.info("startDatabase as kontor in hsqldb_databases/test"); + LocalTestDatabase.server = new Server(); + LocalTestDatabase.server.setAddress("localhost"); + LocalTestDatabase.server.setDatabaseName(0, "kontor"); + LocalTestDatabase.server.setDatabasePath(0, "file:build/hsqldb_databases/test"); + LocalTestDatabase.server.setPort(2345); + LocalTestDatabase.server.setTrace(true); + LocalTestDatabase.server.setLogWriter(new PrintWriter(System.out)); + LocalTestDatabase.server.start(); + } + + private static void stopDatabase() { + server.shutdown(); + } + + private static EntityManagerFactory getFactory() { + if (LocalTestDatabase.factory == null) { + LocalTestDatabase.assureDatabaseRunning(); + PersistenceProviderResolverHolder.setPersistenceProviderResolver(new PersistenceProviderResolver() { + private final List providers_ = Arrays.asList((PersistenceProvider) new HibernatePersistenceProvider()); + + @Override + public void clearCachedProviders() { + // Auto-generated method stub + } + + @Override + public List getPersistenceProviders() { + return providers_; + } + }); + LocalTestDatabase.factory = Persistence.createEntityManagerFactory("com.ibtp.kontor"); + logger.info("EntityManagerFactory(com.ibtp.kontor) created"); + } + return factory; + } + + private static EntityManager getSingleEntityManager() { + return LocalTestDatabase.em; + } + + private static void setSingleEntityManager(EntityManager manager) { + LocalTestDatabase.em = manager; + } + + @Override + public EntityManager getEntityManager() { + if (getSingleEntityManager() == null) { + setSingleEntityManager(getFactory().createEntityManager()); + logger.info("EntityManager created"); + } + return getSingleEntityManager(); + } + + @Override + public String toString() { + String serverMessage; + if (LocalTestDatabase.server == null) { + serverMessage = "server:null"; + } else { + serverMessage = LocalTestDatabase.server.toString(); + } + return LocalTestDatabase.class.getName() + " " + serverMessage; + } +} diff --git a/java/src/test/resources/META-INF/persistence.xml b/java/src/test/resources/META-INF/persistence.xml new file mode 100644 index 0000000..35d671b --- /dev/null +++ b/java/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,39 @@ + + + org.hibernate.jpa.HibernatePersistenceProvider + com.ibtp.kontor.comics.entity.ArtistEntity + com.ibtp.kontor.comics.entity.ComicEntity + com.ibtp.kontor.comics.entity.IssueEntity + com.ibtp.kontor.comics.entity.StoryArcEntity + com.ibtp.kontor.comics.entity.VolumeEntity + com.ibtp.kontor.comics.entity.PublisherEntity + com.ibtp.kontor.library.entity.AuthorEntity + com.ibtp.kontor.library.entity.ArticleEntity + com.ibtp.kontor.library.entity.BookEntity + com.ibtp.kontor.library.entity.FileEntity + com.ibtp.kontor.library.entity.TitleEntity + com.ibtp.kontor.tradingcards.entity.SportEntity + com.ibtp.kontor.tradingcards.entity.TeamEntity + com.ibtp.kontor.tradingcards.entity.PositionEntity + com.ibtp.kontor.tradingcards.entity.PlayerEntity + com.ibtp.kontor.tradingcards.entity.ManufacturerEntity + com.ibtp.kontor.tradingcards.entity.BaseSetEntity + com.ibtp.kontor.tradingcards.entity.InsertEntity + com.ibtp.kontor.tradingcards.entity.ParallelSetEntity + com.ibtp.kontor.tradingcards.entity.SportCardEntity + + + + + + + + + + + + + diff --git a/java/src/test/resources/logback.xml b/java/src/test/resources/logback.xml new file mode 100644 index 0000000..5254e50 --- /dev/null +++ b/java/src/test/resources/logback.xml @@ -0,0 +1,41 @@ + + + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + build/kontortest.log + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + build/kontortest.%i.log.zip + 1 + 10 + + + + 2MB + + + + + + + + + + + + + + diff --git a/java/tysc-20041010-1819.sql b/java/tysc-20041010-1819.sql new file mode 100644 index 0000000..167c41d --- /dev/null +++ b/java/tysc-20041010-1819.sql @@ -0,0 +1,168 @@ +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE=NO_AUTO_VALUE_ON_ZERO */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `tysc`; +USE `tysc`; + +DROP TABLE IF EXISTS `angebote`; +CREATE TABLE `angebote` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `angebote` (`user_id`,`karte_id`) VALUES (3,28),(3,30); + +DROP TABLE IF EXISTS `benutzer`; +CREATE TABLE `benutzer` ( + `ID` int(11) NOT NULL auto_increment, + `forename` varchar(40) default NULL, + `surname` varchar(40) default NULL, + `strasse` varchar(60) default NULL, + `plz` int(6) default NULL, + `ort` varchar(20) default NULL, + `username` varchar(20) default NULL, + `email` varchar(60) default NULL, + `password` varchar(32) default NULL, + `java` char(1) default NULL, + `language` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `benutzer` (`ID`,`forename`,`surname`,`strasse`,`plz`,`ort`,`username`,`email`,`password`,`java`,`language`) VALUES (1,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','gophard','thomas.peetz@snafu.de','t.log1n','N',1),(2,'Heiko','John','Johannastr.49',13581,'Berlin','John','healjo@hotmail.com','redskins','N',1),(3,'Thomas','Peetz','Reichweindamm 24',13627,'Berlin','peetz','gophard@snafu.de','peetz','N',1); + +DROP TABLE IF EXISTS `changelog`; +CREATE TABLE `changelog` ( + `datum` date default NULL, + `tablename` varchar(20) default NULL, + `id` int(11) NOT NULL default '0' +) TYPE=MyISAM; +INSERT INTO `changelog` (`datum`,`tablename`,`id`) VALUES ('2002-02-27','benutzer',1),('2002-02-28','benutzer',2),('2002-03-05','benutzer',3),('2002-03-05','angebote',0),('2002-03-05','angebote',0); + +DROP TABLE IF EXISTS `hersteller`; +CREATE TABLE `hersteller` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `hersteller` (`ID`,`name`) VALUES (1,'Pacific'),(2,'Fleer'),(3,'Bowman'),(6,'Topps'),(7,'Donruss'),(8,'Score'),(9,'Flair'); + +DROP TABLE IF EXISTS `inserts`; +CREATE TABLE `inserts` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `inserts` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Big Buzz'); + +DROP TABLE IF EXISTS `karte`; +CREATE TABLE `karte` ( + `ID` int(11) NOT NULL auto_increment, + `spieler_id` int(11) NOT NULL default '0', + `team_id` int(11) NOT NULL default '0', + `hersteller_id` int(11) NOT NULL default '0', + `serie_id` int(11) default NULL, + `parallel_id` int(11) default NULL, + `inserts_id` int(11) default NULL, + `rookie` char(1) default NULL, + `jahr` int(4) default NULL, + `nummer` int(11) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (12,12,13,1,1,0,0,'N',2001,212),(1,1,2,1,1,0,0,'N',2001,185),(2,2,2,1,1,0,0,'N',2001,250),(3,3,8,1,1,0,0,'N',2001,103),(4,4,8,1,1,0,0,'N',2001,112),(5,5,6,1,1,0,0,'N',2001,37),(6,6,6,1,1,0,0,'N',2001,38),(7,7,6,1,1,0,0,'N',2001,31),(8,8,10,1,1,0,0,'N',2001,338),(9,9,10,1,1,0,0,'N',2001,335),(10,10,10,1,1,0,0,'N',2001,345),(11,11,13,1,1,0,0,'N',2001,213),(13,13,14,1,1,0,0,'N',2001,311),(14,14,14,1,1,0,0,'N',2001,312),(15,15,16,1,1,0,0,'N',2001,403),(16,16,16,1,1,0,0,'N',2001,397),(17,17,16,1,1,0,0,'N',2001,404),(18,18,18,1,1,0,0,'N',2001,116),(19,19,18,1,1,0,0,'N',2001,122),(20,20,18,1,1,0,0,'N',2001,117),(21,21,19,1,1,0,0,'N',2001,281),(22,22,19,1,1,0,0,'N',2001,321),(23,23,20,1,1,0,0,'N',2001,331),(24,24,20,1,1,0,0,'N',2001,324),(25,25,21,1,1,0,0,'N',2001,445),(26,26,27,1,1,0,0,'N',2001,28),(27,27,27,1,1,0,0,'N',2001,17),(28,28,27,1,1,0,0,'N',2001,23),(29,29,29,1,1,0,0,'N',2001,273); +INSERT INTO `karte` (`ID`,`spieler_id`,`team_id`,`hersteller_id`,`serie_id`,`parallel_id`,`inserts_id`,`rookie`,`jahr`,`nummer`) VALUES (30,30,31,1,1,0,0,'N',2001,380),(31,31,31,1,1,0,0,'N',2001,390),(32,32,31,1,1,0,0,'N',2001,381),(33,33,31,1,1,0,0,'N',2001,387),(34,34,31,1,1,0,0,'N',2001,386),(35,35,30,1,1,0,0,'N',2001,349),(36,36,30,1,1,0,0,'N',2001,350),(37,37,44,5,8,0,0,'N',1994,106); + +DROP TABLE IF EXISTS `language`; +CREATE TABLE `language` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `mannschaft`; +CREATE TABLE `mannschaft` ( + `ID` int(11) NOT NULL auto_increment, + `team_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + PRIMARY KEY (`ID`) +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `parallelset`; +CREATE TABLE `parallelset` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `parallelset` (`ID`,`hersteller_id`,`name`) VALUES (1,2,'Mystique Gold'),(2,1,'Pacific Copper'),(3,1,'Pacific Gold'); + +DROP TABLE IF EXISTS `position`; +CREATE TABLE `position` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(20) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `position` (`ID`,`sportart_id`,`name`) VALUES (1,1,'QB'),(2,1,'WR'),(3,1,'RB'),(4,1,'LB'),(5,1,'TE'),(6,1,'FB'),(7,1,'SS'),(8,1,'DE'),(9,1,'K'),(10,1,'P'),(11,1,'LG'),(12,1,'RG'),(13,1,'OF'),(14,1,'DB'),(15,1,'CB'),(16,2,'C'),(17,2,'1B'),(18,2,'2B'),(19,2,'3B'),(20,2,'SS'),(21,2,'LF'),(22,2,'CF'),(23,2,'RF'),(24,2,'DH'),(25,2,'P'); + +DROP TABLE IF EXISTS `serie`; +CREATE TABLE `serie` ( + `ID` int(11) NOT NULL auto_increment, + `hersteller_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`,`hersteller_id`) +) TYPE=MyISAM; +INSERT INTO `serie` (`ID`,`hersteller_id`,`name`) VALUES (1,1,'Pacific'),(2,2,'Fleer'),(3,3,'Bowman'),(4,4,'Leaf'),(5,2,'Ultra'),(6,2,'Mystique'),(7,1,'Finest Hour'),(8,5,'SP'),(9,5,'SPX'),(10,5,'SP Authentic'),(11,5,'Black Diamond'); + +DROP TABLE IF EXISTS `spiele`; +CREATE TABLE `spiele` ( + `datum` date default NULL, + `gast` int(11) NOT NULL default '0', + `gast_pkt` int(11) default NULL, + `heim` int(11) NOT NULL default '0', + `heim_pkt` int(11) default NULL +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `spieler`; +CREATE TABLE `spieler` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(40) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `spieler` (`ID`,`name`) VALUES (1,'Pathon, Jerome'),(2,'Bruschi, Tedy'),(3,'Couch, Tim'),(4,'Shea, Aaron'),(5,'Lewis, Jamal'),(6,'Lewis, Jermaine'),(7,'Banks, Tony'),(8,'Fuamatu-Ma\'Afala, Chris'),(9,'Bettis, Jerome'),(10,'Stewart, Kordell'),(11,'Moon, Warren'),(12,'Lockett, Kevin'),(13,'Gannon, Rich'),(14,'Jett, James'),(15,'Strong, Mack'),(16,'Huard, Brock'),(17,'Watters, Ricky'),(18,'Aikman, Troy'),(19,'LaFleur, David'),(20,'Brazzell, Chris'),(21,'Dayne, Ron'),(22,'Brown, Na'),(23,'Small, Torrance'),(24,'Lewis, Chad'),(25,'Murrell, Adrian'),(26,'Smith, Maurice'),(27,'Chandler, Chris'),(28,'Kanell, Danny'),(29,'Williams, Ricky'),(30,'Garcia, Jeff'),(31,'Streets, Tai'),(32,'Garner, Charlie'),(33,'Rice, Jerry'),(34,'Owens, Terrell'),(35,'Bruce, Isaac'),(36,'Canidate, Trung'); + +DROP TABLE IF EXISTS `spielerposition`; +CREATE TABLE `spielerposition` ( + `spieler_id` int(11) NOT NULL default '0', + `sportart_id` int(11) NOT NULL default '0', + `position_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `sportart`; +CREATE TABLE `sportart` ( + `ID` int(11) NOT NULL auto_increment, + `name` varchar(30) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `sportart` (`ID`,`name`) VALUES (1,'Football'),(2,'Baseball'),(3,'Basketball'),(4,'Hockey'); + +DROP TABLE IF EXISTS `suche`; +CREATE TABLE `suche` ( + `user_id` int(11) NOT NULL default '0', + `karte_id` int(11) NOT NULL default '0' +) TYPE=MyISAM; + +DROP TABLE IF EXISTS `team`; +CREATE TABLE `team` ( + `ID` int(11) NOT NULL auto_increment, + `sportart_id` int(11) NOT NULL default '0', + `name` varchar(40) default NULL, + `short` varchar(15) default NULL, + PRIMARY KEY (`ID`) +) TYPE=MyISAM; +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (1,1,'Buffalo Bills','Bills'),(2,1,'Indianapolis Colts','Colts'),(3,1,'Miami Dolphins','Dolphins'),(4,1,'New England Patriots','Patriots'),(5,1,'New York Jets','Jets'),(6,1,'Baltimore Ravens','Ravens'),(7,1,'Cincinnati Bengals','Bengals'),(8,1,'Cleveland Browns','Browns'),(9,1,'Jacksonville Jaguars','Jaguars'),(10,1,'Pittsburgh Steelers','Steelers'),(11,1,'Tennessee Titans','Titans'),(12,1,'Denver Broncos','Broncos'),(13,1,'Kansas City Chiefs','Chiefs'),(14,1,'Oakland Raiders','Raiders'),(15,1,'San Diego Chargers','Chargers'),(16,1,'Seattle Seahawks','Seahawks'),(17,1,'Arizona Cardinals','Cardinals'),(18,1,'Dallas Cowboys','Cowboys'),(19,1,'New York Giants','Giants'),(20,1,'Philadelphia Eagles','Eagles'),(21,1,'Washington Redskins','Redskins'),(22,1,'Chicago Bears','Bears'),(23,1,'Detroit Lions','Lions'),(24,1,'Green Bay Packers','Packers'),(25,1,'Minnesota Vikings','Vikings'),(26,1,'Tampa Bay Buccaneers','Buccaneers'),(27,1,'Atlanta Falcons','Falcons'),(28,1,'Carolina Panthers','Panthers'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (29,1,'New Orleans Saints','Saints'),(30,1,'St.Louis Rams','Rams'),(31,1,'San Francisco 49ers','49ers'),(32,2,'Baltimore Orioles','Orioles'),(33,2,'Boston Red Sox','Red Sox'),(34,2,'New York Yankees','Yankees'),(35,2,'Tampa Bay Devil Rays','Devil Rays'),(36,2,'Toronto Blue Jays','Blue Jays'),(37,2,'Chicago White Sox','White Sox'),(38,2,'Cleveland Indians','Indians'),(39,2,'Detroit Tigers','Tigers'),(40,2,'Kansas City Royals','Royals'),(41,2,'Minnesota Twins','Twins'),(42,2,'Anaheim Angels','Angels'),(43,2,'Oakland Athletics','Athletics'),(44,2,'Seattle Mariners','Mariners'),(45,2,'Texas Rangers','Rangers'),(46,2,'Atlanta Braves','Braves'),(47,2,'Florida Marlins','Marlins'),(48,2,'Montreal Expos','Expos'),(49,2,'New York Mets','Mets'),(50,2,'Philadelphia Phillies','Phillies'),(51,2,'Chicago Cubs','Cubs'),(52,2,'Cincinnati Reds','Reds'),(53,2,'Houston Astros','Astros'),(54,2,'Milwaukee Brewers','Brewers'),(55,2,'Pittsburgh Pirates','Pirates'),(56,2,'St.Louis Cardinals','Cardinals'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (57,2,'Arizona Diamondbacks','Diamondbacks'),(58,2,'Colorado Rockies','Rockies'),(59,2,'Los Angeles Dodgers','Dodgers'),(60,2,'San Diego Padres','Padres'),(61,2,'San Francisco Giants','Giants'),(62,3,'Boston Celtics','Celtics'),(63,3,'Miami Heat','Heat'),(64,3,'New Jersey Nets','Mets'),(65,3,'New York Knicks','Knicks'),(66,3,'Orlando Magic','Magic'),(67,3,'Philadelphia 76ers','76ers'),(68,3,'Washington Wizards','Wizards'),(69,3,'Atlanta Hawks','Hawks'),(70,3,'Charlotte Hornets','Hornets'),(71,3,'Chicago Bulls','Bulls'),(72,3,'Cleveland Cavaliers','Cavaliers'),(73,3,'Detroit Pistons','Pistons'),(74,3,'Indiana Pacers','Pacers'),(75,3,'Milwaukee Bucks','Bucks'),(76,3,'Toronto Raptors','Raptors'),(77,3,'Dallas Mavericks','Mavericks'),(78,3,'Denver Nuggets','Nuggets'),(79,3,'Houston Rockets','Rockets'),(80,3,'Minnesota Timberwolves','Timberwolves'),(81,3,'San Antonio Spurs','Spurs'),(82,3,'Utah Jazz','Jazz'),(83,3,'Vancouver Grizzlies','Grizzlies'),(84,3,'Golden State Warriors','Warriors'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (85,3,'Los Angeles Clippers','Clippers'),(86,3,'Los Angeles Lakers','Lakers'),(87,3,'Phoenix Suns','Suns'),(88,3,'Portland Trail Blazers','Blazers'),(89,3,'Sacramento Kings','Kings'),(90,3,'Seattle SuperSonics','SuperSonics'),(91,4,'Boston Bruins','Bruins'),(92,4,'Buffalo Sabres','Sabres'),(93,4,'Montreal Canadiens','Canadiens'),(94,4,'Ottawa Senators','Senators'),(95,4,'Toronto Maple Leafs','Maple Leafs'),(96,4,'New Jersey Devils','Devils'),(97,4,'New York Islander','Islander'),(98,4,'New York Rangers','Rangers'),(99,4,'Philadelphia Flyers','Flyers'),(100,4,'Pittsburgh Penguins','Penguins'),(101,4,'Atlanta Trashers','Trashers'),(102,4,'Carolina Hurricanes','Hurricanes'),(103,4,'Florida Panthers','Panthers'),(104,4,'Tampa Bay Lightnings','Lightnings'),(105,4,'Washington Capitals','Capitals'),(106,4,'Chicago Blackhawks','Blackhawks'),(107,4,'Columbo Blue Jackets','Blue Jackets'),(108,4,'Detroit Red Wings','Red Wings'),(109,4,'Nashville Predators','Predators'),(110,4,'St.Louis Blues','Blues'); +INSERT INTO `team` (`ID`,`sportart_id`,`name`,`short`) VALUES (111,4,'Calgary Flames','Flames'),(112,4,'Colorado Avalanche','Avalanche'),(113,4,'Edmonton Oilers','Oilers'),(114,4,'Minnesota Wild','Wild'),(115,4,'Vancouver Canucks','Canucks'),(116,4,'Anaheim Mighty Ducks','Mighty Ducks'),(117,4,'Dallas Stars','Stars'),(118,4,'Los Angeles Kings','Kings'),(119,4,'Phoenix Coyotes','Coyotes'),(120,4,'San Jose Sharks','Sharks'),(121,1,'Houston Texans','Texans'),(122,1,'Houston Oilers','Oilers'); +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; diff --git a/kontor-api/.coverage b/kontor-api/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..44d6ca261d7b921a433a3387e3cb7b53e796297f GIT binary patch literal 53248 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 OK*m7AAcX=4.13.4", + "fastapi[standard]>=0.115.12", + "httpx==0.24.1", + "mariadb>=1.1.12", + "sqlalchemy>=2.0.40", + "platformdirs>=4.3.7", + "pathlib>=1.0.1", + "platformdirs>=4.3.7", + "pytest==7.4.0", + "pyyaml>=6.0.2", + "requests>=2.32.3", + "sqlalchemy>=2.0.40", + "sqlmodel>=0.0.24", + "python-dotenv>=1.1.0", + "python-jose>=3.4.0", + "python-multipart>=0.0.20", + "natsort>=8.4.0", +] diff --git a/kontor-api/src/__init__.py b/kontor-api/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/apis/__init__.py b/kontor-api/src/apis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/apis/base.py b/kontor-api/src/apis/base.py new file mode 100644 index 0000000..e969691 --- /dev/null +++ b/kontor-api/src/apis/base.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from src.apis.version1 import comic, media, tysc + +api_router = APIRouter(prefix="/api") +api_router.include_router(comic.router, prefix="/comics", tags=["comics"]) +api_router.include_router(media.router, prefix="/media", tags=["media"]) +api_router.include_router(tysc.router, prefix="/tysc", tags=["tysc"]) diff --git a/kontor-api/src/apis/utils.py b/kontor-api/src/apis/utils.py new file mode 100644 index 0000000..716d2be --- /dev/null +++ b/kontor-api/src/apis/utils.py @@ -0,0 +1,8 @@ +from typing import Annotated + +from fastapi import Depends +from sqlalchemy.orm import Session + +from src.db.session import get_db + +SessionDep = Annotated[Session, Depends(get_db)] diff --git a/kontor-api/src/apis/version1/__init__.py b/kontor-api/src/apis/version1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/apis/version1/comic.py b/kontor-api/src/apis/version1/comic.py new file mode 100644 index 0000000..a558b3b --- /dev/null +++ b/kontor-api/src/apis/version1/comic.py @@ -0,0 +1,62 @@ +from uuid import UUID +from typing import List +from fastapi import APIRouter, HTTPException, status +from sqlalchemy import select + +from src.apis.utils import SessionDep +from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info +from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse, get_artist_details +from src.db.models.comic import Comic, Artist + +router = APIRouter( + prefix="/comic", + tags=["comics"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/comics") +def get_all_comics(db: SessionDep) -> List[ComicResponse]: + results: List[ComicResponse] = [] + comics = db.scalars(select(Comic)).all() + for comic in comics: + response = get_short_info(comic) + results.append(response) + return results + +@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse) +def get_comic(comic_id: UUID, db: SessionDep) -> ComicDetailsResponse: + comic = db.get(Comic, comic_id) + if comic is None: + raise HTTPException(status_code=404, detail="Comic could not be found") + response: ComicDetailsResponse = get_comic_details(comic) + return response + +@router.get("/artists", response_model=List[ArtistResponse]) +def get_all_artists(db: SessionDep) -> List[ArtistResponse]: + results: List[ArtistResponse] = [] + artists = db.query(Artist).all() + for artist in artists: + results.append(ArtistResponse(id=artist.id, name=artist.name)) + return results + +@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse) +def get_artist(artist_id: UUID, db: SessionDep) -> ArtistDetailResponse: + artist = db.get(Artist, artist_id) + if artist is None: + raise HTTPException(status_code=404, detail="Artist could not be found") + response: ArtistDetailResponse = get_artist_details(artist) + return response + +@router.post("/artists", status_code=status.HTTP_201_CREATED) +def add_artist(db: SessionDep, artist_creation: ArtistCreation) -> ArtistResponse: + artist: Artist = Artist() + setattr(artist, "name", artist_creation.name) + try: + db.add(artist) + db.commit() + except: + raise HTTPException(status_code=409, detail="Artist already added") + response = ArtistResponse(id=artist.id, name=artist.name) + return response + diff --git a/kontor-api/src/apis/version1/media.py b/kontor-api/src/apis/version1/media.py new file mode 100644 index 0000000..2fa7347 --- /dev/null +++ b/kontor-api/src/apis/version1/media.py @@ -0,0 +1,76 @@ +from typing import List +from uuid import UUID + +from fastapi import APIRouter, status, HTTPException +from sqlalchemy import select, Sequence + +from src.apis.utils import SessionDep +from src.schema.media.file import MediaFileResponse, Link, get_file_details, set_file +from src.db.models.media import MediaFile + +router = APIRouter( + prefix="/media", + tags=["media"] +) + +@router.get("/update-titles") +def update_titles(db: SessionDep) -> list[MediaFileResponse]: + results: list[MediaFileResponse] = [] + files = db.query(MediaFile).filter(MediaFile.review == 1).all() + for mediafile in files: + mediafile.update_title() + db.add(mediafile) + response = get_file_details(mediafile) + results.append(response) + db.commit() + return results + + +@router.get("/files", response_model=List[MediaFileResponse]) +def get_all_files(db: SessionDep, review: bool = False, download: bool = False) -> List[MediaFileResponse]: + results: list[MediaFileResponse] = [] + files: Sequence[MediaFile] + if review: + files = db.query(MediaFile).filter(MediaFile.review == 1).all() + elif download: + files = db.query(MediaFile).filter(MediaFile.should_download == 1).all() + else: + files = db.scalars(select(MediaFile)).all() + for mediafile in files: + response = get_file_details(mediafile) + results.append(response) + return results + +@router.get("/files/{file_id}", response_model=MediaFileResponse) +def get_file(file_id: UUID, db: SessionDep) -> MediaFileResponse: + mediafile = db.get(MediaFile, file_id) + if not mediafile: + raise HTTPException(status_code=404, detail="MediaFile could not be found") + response = get_file_details(mediafile) + return response + +@router.put("/files/{file_id}", response_model=MediaFileResponse) +def update_file(file_id: UUID, db: SessionDep, info: MediaFileResponse) -> MediaFileResponse: + mediaFile = db.get(MediaFile, file_id) + if not mediaFile: + raise HTTPException(status_code=404, detail="MediaFile could not be found") + set_file(info, mediaFile) + db.add(mediaFile) + db.commit() + return info + + +@router.post("/files", status_code=status.HTTP_201_CREATED) +def add_file(new_link: Link, db: SessionDep) -> MediaFileResponse: + print(new_link.url) + try: + mediaFile: MediaFile = MediaFile() + setattr(mediaFile, "url", new_link.url) + setattr(mediaFile, "review", 1) + setattr(mediaFile, "should_download", 1) + db.add(mediaFile) + db.commit() + except: + raise HTTPException(status_code=409, detail="Link duplicate") + response = get_file_details(mediaFile) + return response diff --git a/kontor-api/src/apis/version1/tysc.py b/kontor-api/src/apis/version1/tysc.py new file mode 100644 index 0000000..1128771 --- /dev/null +++ b/kontor-api/src/apis/version1/tysc.py @@ -0,0 +1,20 @@ +from typing import List +from fastapi import APIRouter + +from src.apis.utils import SessionDep +from src.schema.tysc.sport import SportResponse +from src.db.models.tysc import Sport + +router = APIRouter( + prefix="/tysc", + tags=["tysc"], + responses={404: {"description": "Not found"}}, +) + +@router.get("/sports") +def get_all_sports(db: SessionDep) -> List[SportResponse]: + results: list[SportResponse] = [] + sports = db.query(Sport).all() + for sport in sports: + results.append(SportResponse(id=sport.id, name=sport.name)) + return results diff --git a/kontor-api/src/core/__init__.py b/kontor-api/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/core/config.py b/kontor-api/src/core/config.py new file mode 100644 index 0000000..5f14424 --- /dev/null +++ b/kontor-api/src/core/config.py @@ -0,0 +1,23 @@ +import os +from pathlib import Path + +from dotenv import load_dotenv + +env_path = Path(".") / ".env" +load_dotenv(dotenv_path=env_path) + + +class Settings: + PROJECT_NAME: str = "Kontor" + PROJECT_VERSION: str = "0.1.0" + + MARIADB_USER: str = os.getenv("MARIADB_USER", "kontor") + MARIADB_PASSWORD: str = os.getenv("MARIADB_PASSWORD", "kontor") + MARIADB_SERVER: str = os.getenv("MARIADB_SERVER", "mariadb") + MARIADB_PORT: str = os.getenv("MARIADB_PORT", 3306) + MARIADB_DB: str = os.getenv("MARIADB_DB", "kontor") + DATABASE_URL: str = f"mariadb+mariadbconnector://{MARIADB_USER}:{MARIADB_PASSWORD}@{MARIADB_SERVER}:{MARIADB_PORT}/{MARIADB_DB}" + + +settings = Settings() + diff --git a/kontor-api/src/db/__init__.py b/kontor-api/src/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/db/models/__init__.py b/kontor-api/src/db/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/db/models/admin.py b/kontor-api/src/db/models/admin.py new file mode 100644 index 0000000..e16b0e0 --- /dev/null +++ b/kontor-api/src/db/models/admin.py @@ -0,0 +1,78 @@ +from datetime import datetime + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship, mapped_column, Mapped + +from src.db.models.base import Base, BaseMixin + + +class Profile(Base, BaseMixin): + __tablename__ = 'profile' + first_name = Column(String(255)) + last_name = Column(String(255)) + user_name = Column(String(255), nullable=False) + email = Column(String(255)) + password = Column(String(255)) + enabled = Column(BIT(1)) + assignments = relationship("Assignment") + tokens = relationship("Token") + + def get_full_name(self) -> str: + full_name = "" + if self.first_name is not None: + full_name += self.first_name + if self.last_name is not None: + if len(full_name) > 0: + full_name += " " + full_name += self.last_name + return full_name + + +class Token(Base, BaseMixin): + __tablename__ = "token" + token = Column(String(255), nullable=False, unique=True) + name = Column(String(255)) + last_used_date: Mapped[datetime] = mapped_column() + enabled = Column(BIT(1)) + profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False) + profile = relationship("Profile", back_populates="tokens") + + +class Permission(Base, BaseMixin): + __tablename__ = "permission" + name = Column(String(255), nullable=False) + assignments = relationship("Assignment") + + +class Assignment(Base, BaseMixin): + __tablename__ = "assignment" + profile_id = Column(String, ForeignKey("profile.id"), nullable=False) + profile = relationship("Profile", back_populates="assignments") + permission_id = Column(String, ForeignKey("permission.id"), nullable=False) + permission = relationship("Permission", back_populates="assignments") + + +class ModuleData(Base, BaseMixin): + __tablename__ = "module_data" + module_name = Column(String(255), nullable=False) + import_data = Column(BIT(1)) + + +class MailAccount(Base, BaseMixin): + __tablename__ = "mail_account" + host = Column(String(255)) + port = Column(Integer) + protocol = Column(String(255)) + user_name = Column(String(255)) + password = Column(String(255)) + start_tls = Column(BIT(1)) + + +class Mail(Base, BaseMixin): + __tablename__ = "mail" + folder: Mapped[str] = mapped_column() + subject: Mapped[str] = mapped_column() + body: Mapped[str] = mapped_column() + sent_date: Mapped[datetime] = mapped_column() + received_date: Mapped[datetime] = mapped_column() diff --git a/kontor-api/src/db/models/base.py b/kontor-api/src/db/models/base.py new file mode 100644 index 0000000..4a354e7 --- /dev/null +++ b/kontor-api/src/db/models/base.py @@ -0,0 +1,31 @@ +import uuid +from datetime import datetime + +from sqlalchemy import func, Column, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + pass + + +class BaseMixin: + id = Column(String(255), primary_key=True, default=uuid.uuid4()) + # id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) + # created_date = Column(DateTime) + created_date: Mapped[datetime] = mapped_column(default=func.now()) + # last_modified_date = Column(DateTime) + last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) + # version = Column(Integer) + version: Mapped[int] = mapped_column(default=0) + + +class BaseVideoMixin: + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) diff --git a/kontor-api/src/db/models/bookshelf.py b/kontor-api/src/db/models/bookshelf.py new file mode 100644 index 0000000..d01fc6b --- /dev/null +++ b/kontor-api/src/db/models/bookshelf.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from src.db.models.base import Base, BaseMixin + + +class Article(Base, BaseMixin): + __tablename__ = 'article' + title = Column(String(length=255), unique=True) + article_authors = relationship("ArticleAuthor") + + +class Author(Base, BaseMixin): + __tablename__ = 'author' + first_name = Column(String(255)) + last_name = Column(String(255)) + article_authors = relationship("ArticleAuthor") + book_authors = relationship("BookAuthor") + + +class BookshelfPublisher(Base, BaseMixin): + __tablename__ = 'bookshelf_publisher' + name = Column(String(length=255), unique=True) + books = relationship("Book") + + +class Book(Base, BaseMixin): + __tablename__ = 'book' + isbn = Column(String(255), unique=True) + title = Column(String(255)) + year = Column(Integer, nullable=False) + publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) + publisher = relationship('BookshelfPublisher', back_populates="books") + book_authors = relationship("BookAuthor") + + +class ArticleAuthor(Base, BaseMixin): + __tablename__ = 'article_author' + article_id = Column(String, ForeignKey('article.id'), nullable=False) + article = relationship('Article', back_populates="article_authors") + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="article_authors") + + +class BookAuthor(Base, BaseMixin): + __tablename__ = 'book_author' + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="book_authors") + book_id = Column(String, ForeignKey('book.id'), nullable=False) + book = relationship('Book', back_populates="book_authors") diff --git a/kontor-api/src/db/models/comic.py b/kontor-api/src/db/models/comic.py new file mode 100644 index 0000000..423ad7c --- /dev/null +++ b/kontor-api/src/db/models/comic.py @@ -0,0 +1,128 @@ +from typing import Dict, List +from natsort import natsorted +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from src.db.models.base import Base, BaseMixin + + +class Publisher(Base, BaseMixin): + __tablename__ = "publisher" + name = Column(String(length=255), unique=True) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base, BaseMixin): + __tablename__ = 'comic' + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + issues = relationship("Issue", order_by="Issue.issue_number") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' + + def get_artists(self) -> Dict[str, List[str]]: + works: Dict[str, List[str]] = {} + for work in self.comic_works: + work_type = work.work_type.name + artist = work.artist + if work_type in works: + works[work_type].append(artist) + else: + works[work_type] = [artist] + return works + + def sorted_issues(self): + sorted_issues = natsorted(self.issues, key=lambda x: getattr(x, 'issue_number')) + return sorted_issues + + +class Volume(Base, BaseMixin): + __tablename__ = "volume" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base, BaseMixin): + __tablename__ = "trade_paperback" + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base, BaseMixin): + __tablename__ = "story_arc" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base, BaseMixin): + __tablename__ = "issue" + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base, BaseMixin): + __tablename__ = "artist" + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + def get_comics(self) -> Dict[str, List[str]]: + works: Dict[str, List[str]] = {} + for work in self.comic_works: + work_type = work.work_type.name + comic = work.comic + if work_type in works: + works[work_type].append(comic) + else: + works[work_type] = [comic] + return works + + +class WorkType(Base, BaseMixin): + __tablename__ = "worktype" + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})' + + def __str__(self): + return f'{self.name}({self.id})' + + +class ComicWork(Base, BaseMixin): + __tablename__ = "comic_work" + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/kontor-api/src/db/models/database.py b/kontor-api/src/db/models/database.py new file mode 100644 index 0000000..0d5998b --- /dev/null +++ b/kontor-api/src/db/models/database.py @@ -0,0 +1,396 @@ +import json +import logging +import uuid +from datetime import datetime +from enum import Enum, auto +from pathlib import Path +from typing import Any + +from sqlalchemy import select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from src.db.models.tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport +from src.db.models.comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType +from src.db.models.bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author +from src.db.models.admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix +from src.db.models.metadata import MetaDataTable, MetaDataColumn +from src.db.models.media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile + + +class ColumnEntry(Enum): + COLUMN_NAME = 'column' + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = 'type' + COLUMN_WIDGET = 'widget' + + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + +class ExportType(Enum): + JSON = "JSON" + YAML = "YAML" + SQLITE = "SQLite" + + +class KontorDB: + + def __init__(self, db_engine: Any): + self.engine = db_engine + self.registry = {} + self.init_registry() + + def init_registry(self): + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaActor.__tablename__] = MediaActor + self.registry[MediaActorFile.__tablename__] = MediaActorFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[Token.__tablename__] = Token + self.registry[User.__tablename__] = User + self.registry[Role.__tablename__] = Role + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail + + def get_table_names(self) -> list: + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = session.scalars(select(MetaDataTable)).all() + result = [table.table_name for table in tables] + return result + + def get_table_by_name(self, table_name: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + _filter = {'table_name': table_name} + with __session__() as session: + table = session.query(MetaDataTable).filter_by(**_filter).one() + result['id'] = table.id + result['table_name'] = table.table_name + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + __session__ = sessionmaker(self.engine) + columns = list() + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + if view_only: + _filters['is_shown'] = True + with __session__() as session: + columns = session.query(MetaDataColumn).filter_by(**_filters).all() + for column in columns: + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + return meta_data + + def get_columns(self, table_name: str) -> dict: + columns = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } + return columns + + def get_filters(self, table_name: str) -> dict: + _filter_map = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id'], 'show_filter': True} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } + return _filter_map + + def data(self, table_name: str, columns: dict, filters: dict) -> list: + data = [] + __session__ = sessionmaker(self.engine) + table = self.registry[table_name] + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.scalars(select(table)).all() + else: + entries = session.scalars(select(table).filter_by(**filters)).all() + for entry in entries: + # self.log.info("data: %s", entry) + row = [] + for order in columns.keys(): + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + ref = getattr(entry, ref_table) + value = getattr(ref, ref_column) + row.append(value) + else: + row.append(getattr(entry, column_name)) + data.append(row) + # self.log.info("data: %s", data) + return data + + def export_db(self, export_type: str, export_file_name: str) -> dict: + results = {} + db = {} + export_table_list = self.get_table_names() + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + logging.info(f"table {table} is not registered") + continue + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order][ColumnEntry.COLUMN_NAME] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError: + pass + entries.append(entry) + db[table] = entries + results[table] = len(entries) + match export_type: + case "JSON": + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case "YAML": + pass + case "SQLite": + pass + logging.info(f"{len(results)} tables exported") + return results + + def import_db(self, import_file_name: str) -> dict: + result = {} + import_file = Path(import_file_name) + if not import_file.exists(): + logging.info(f"File {import_file_name} does not exist. Do nothing.") + return result + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + logging.info(f"{table}: {len(json_load[table])}") + result[table] = self.import_table(table, json_load[table]) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + return result + + def import_table(self, table_name: str, items:list) -> dict: + result = {} + updated = [] + added = [] + remaining = [] + existing_ids = self.get_ids(table_name) + logging.info(f"found {len(existing_ids)} existing ids for table {table_name}") + for item in items: + current_id = item['id'] + # print(f"import item: {item}") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + logging.info(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + logging.info(f"Could not add item, due to: {error.detail}") + if len(existing_ids) > 0: + print(f"remaining items for {table_name}: {existing_ids}") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def add_entry(self, table_name: str, update_item: dict): + logging.debug(f"add entry to table {table_name} with {update_item}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() + + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + # self.log.info("update entry to table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + logging.info(f"{key} has changed: {existing_value} != {update_value}") + setattr(existing_item, key, update_value) + session.commit() + changed = True + logging.info(f"update {key} with {update_value}") + return changed + + def add_link(self, link: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} + except IntegrityError as error: + session.rollback() + result['error'] = error.orig + return result + + def update_titles(self) -> dict: + update_list = {} + __session__ = sessionmaker(self.engine) + _filter = { 'review': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + self.log.info("%d entries found for updating titles", len(links)) + for link in links: + url = link.url + if url is None: + continue + link.update_title() + session.commit() + update_list[link.id] = link.title + return update_list + + def get_download_list(self) -> list[uuid.UUID]: + download_list = [] + __session__ = sessionmaker(self.engine) + _filter = { 'should_download': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + for link in links: + url = link.url + if url is None: + continue + download_list.append(link.id) + return download_list + + def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + # self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() + + def check_files(self): + pass diff --git a/kontor-api/src/db/models/media.py b/kontor-api/src/db/models/media.py new file mode 100644 index 0000000..4c0b21a --- /dev/null +++ b/kontor-api/src/db/models/media.py @@ -0,0 +1,101 @@ +import logging +import re +import subprocess +from datetime import datetime +from pathlib import Path + +import requests +from bs4 import BeautifulSoup +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from src.db.models.base import Base, BaseMixin, BaseVideoMixin + + +class MediaFile(Base, BaseMixin, BaseVideoMixin): + __tablename__ = 'media_file' + media_actor_files = relationship("MediaActorFile") + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' + + def update_title(self) -> None: + logging.info(f"update title for {self.url}") + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + self.title = title + self.review = 0 + except: + self.title = None + self.review = 1 + self.last_modified_date = datetime.now() + + def download_file(self, download_dir: str, dl_tool: str): + logging.info(f"download file for {self.url} to {download_dir}") + result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + file_name = self.__parse_output__(lines_list) + if file_name is None: + self.review = 1 + self.should_download = 1 + self.file_name = None + else: + download_file = Path(file_name) + self.should_download = 0 + self.file_name = download_file.name + self.cloud_link = str(download_file.absolute()) + self.last_modified_date = datetime.now() + + def __parse_output__(self, lines_list): + self.file_name = None + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + self.file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + self.file_name = line[-file_len:] + return self.file_name + + +class MediaActor(Base, BaseMixin): + __tablename__ = 'media_actor' + name = Column(String(255)) + media_actor_files = relationship("MediaActorFile") + + +class MediaActorFile(Base, BaseMixin): + __tablename__ = 'media_actor_file' + media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False) + media_actor = relationship("MediaActor", back_populates="media_actor_files") + media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True) + media_file = relationship("MediaFile", back_populates="media_actor_files") + + +class MediaArticle(Base, BaseMixin): + __tablename__ = 'media_article' + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + + +class MediaVideo(Base, BaseMixin): + __tablename__ = 'media_video' + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) diff --git a/kontor-api/src/db/models/metadata.py b/kontor-api/src/db/models/metadata.py new file mode 100644 index 0000000..081c3af --- /dev/null +++ b/kontor-api/src/db/models/metadata.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, String, ForeignKey, Integer +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from src.db.models.base import Base, BaseMixin + + +class MetaDataTable(Base, BaseMixin): + __tablename__ = 'meta_data_table' + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + return f'MetaDataTable({self.id} {self.table_name})' + + def __str__(self): + return f'{self.table_name}({self.id})' + + +class MetaDataColumn(Base, BaseMixin): + __tablename__ = 'meta_data_column' + column_name = Column(String(255), nullable=False) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + column_modifier = Column(String(255), nullable=True) + column_order = Column(Integer) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) + + def __repr__(self): + if self.column_name is None: + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' + else: + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' + + def __str__(self): + return f'{self.column_name}({self.id})' diff --git a/kontor-api/src/db/models/tysc.py b/kontor-api/src/db/models/tysc.py new file mode 100644 index 0000000..dae0417 --- /dev/null +++ b/kontor-api/src/db/models/tysc.py @@ -0,0 +1,100 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from src.db.models.base import Base, BaseMixin + + +class Sport(Base, BaseMixin): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base, BaseMixin): + __tablename__ = "team" + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) + sport = relationship("Sport", back_populates="teams") + roosters = relationship("Rooster") + + +class FieldPosition(Base, BaseMixin): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base, BaseMixin): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) + roosters = relationship("Rooster") + + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + + +class Rooster(Base, BaseMixin): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) + position = relationship("FieldPosition", back_populates="roosters") + cards = relationship("Card") + + +class Vendor(Base, BaseMixin): + __tablename__ = "vendor" + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base, BaseMixin): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base, BaseMixin): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("CardSet", back_populates="cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("Rooster", back_populates="cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/kontor-api/src/db/session.py b/kontor-api/src/db/session.py new file mode 100644 index 0000000..713deb6 --- /dev/null +++ b/kontor-api/src/db/session.py @@ -0,0 +1,15 @@ +from typing import Generator, Annotated + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session + +from src.core.config import settings + +SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL +engine = create_engine(SQLALCHEMY_DATABASE_URL) + +SessionLocal = sessionmaker(bind=engine) + +def get_db() -> Generator: + with SessionLocal() as db: + yield db diff --git a/kontor-api/src/main.py b/kontor-api/src/main.py new file mode 100644 index 0000000..749d3b4 --- /dev/null +++ b/kontor-api/src/main.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from src.apis.base import api_router +from src.db.session import engine +from src.webapps.base import api_router as web_app_router +from src.core.config import settings +from src.db.models.base import Base + +def include_router(app: FastAPI): + app.include_router(api_router) + app.include_router(web_app_router) + +def configure_static(app: FastAPI): + app.mount("/static", StaticFiles(directory="src/static"), name="static") + +def create_tables(): + Base.metadata.create_all(bind=engine) + +def start_application(): + app = FastAPI(title=settings.PROJECT_NAME, version=settings.PROJECT_VERSION) + include_router(app) + configure_static(app) + create_tables() + return app + + +kontor = start_application() + diff --git a/kontor-api/src/schema/__init__.py b/kontor-api/src/schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/schema/comics/__init__.py b/kontor-api/src/schema/comics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/schema/comics/artist.py b/kontor-api/src/schema/comics/artist.py new file mode 100644 index 0000000..1b28812 --- /dev/null +++ b/kontor-api/src/schema/comics/artist.py @@ -0,0 +1,36 @@ +from typing import List, Dict +from uuid import UUID + +from pydantic import BaseModel + +from src.db.models.comic import Artist + + +class ArtistCreation(BaseModel): + name: str + +class ArtistResponse(BaseModel): + id: UUID + name: str + +class ArtistDetailResponse(BaseModel): + id: UUID + name: str + works: Dict[str, List[str]] + +def get_artist_details(artist: Artist) -> ArtistDetailResponse: + works = {} + for work in artist.comic_works: + work_type = work.work_type.name + comic_title = work.comic.title + if work_type in works: + works[work_type].append(comic_title) + else: + works[work_type] = [comic_title] + response = ArtistDetailResponse( + id=artist.id, + name=artist.name, + works=works + ) + return response + diff --git a/kontor-api/src/schema/comics/comic.py b/kontor-api/src/schema/comics/comic.py new file mode 100644 index 0000000..feec39d --- /dev/null +++ b/kontor-api/src/schema/comics/comic.py @@ -0,0 +1,56 @@ +from typing import List, Dict +from uuid import UUID + +from pydantic import BaseModel + +from src.db.models.comic import Comic + + +class ComicResponse(BaseModel): + id: UUID + title: str + completed: bool + +class ComicDetailsResponse(BaseModel): + id: UUID + created: str + title: str + completed : bool + current_order : bool + publisher: str + volumes: List[str] + works: Dict[str, List[str]] + +def get_short_info(comic: Comic) -> ComicResponse: + response = ComicResponse( + id=comic.id, + title=comic.title, + completed=(comic.completed == 1) + ) + return response + + +def get_comic_details(comic: Comic) -> ComicDetailsResponse | None: + volumes = [] + for volume in comic.volumes: + volumes.append(volume.name) + works = {} + for work in comic.comic_works: + work_type = work.work_type.name + artist_name = work.artist.name + if work_type in works: + works[work_type].append(artist_name) + else: + works[work_type] = [artist_name] + response = ComicDetailsResponse( + id=comic.id, + created=str(comic.created_date), + title=comic.title, + completed=(comic.completed == 1), + current_order=(comic.current_order == 1), + publisher=comic.publisher.name, + volumes=volumes, + works=works + ) + return response + diff --git a/kontor-api/src/schema/media/__init__.py b/kontor-api/src/schema/media/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/schema/media/file.py b/kontor-api/src/schema/media/file.py new file mode 100644 index 0000000..2c468e8 --- /dev/null +++ b/kontor-api/src/schema/media/file.py @@ -0,0 +1,45 @@ +from datetime import datetime +from uuid import UUID + +from src.db.models.media import MediaFile +from pydantic import BaseModel + + +class MediaFileResponse(BaseModel): + id: UUID + title: str | None = None + file_name: str | None = None + cloud_link: str | None = None + url: str + review: bool = False + should_download: bool = False + +class Link(BaseModel): + url: str + +def get_file_details(mediafile: MediaFile) -> MediaFileResponse | None: + response = MediaFileResponse(id=mediafile.id, + title=mediafile.title, + file_name=mediafile.file_name, + cloud_link=mediafile.cloud_link, + url=str(mediafile.url), + review=(mediafile.review == 1), + should_download=(mediafile.should_download == 1)) + #print(f"id: {mediafile.id}: review: {response.review} <- {mediafile.review}") + #print(f"id: {mediafile.id}: download: {response.should_download} <- {mediafile.should_download}") + return response + +def set_file(model: MediaFileResponse, mediafile: MediaFile) -> None: + mediafile.file_name = model.file_name + mediafile.cloud_link = model.cloud_link + mediafile.url = model.url + mediafile.title = model.title + mediafile.last_modified_date = datetime.now() + if model.review: + mediafile.review = 1 + else: + mediafile.review = 0 + if model.should_download: + mediafile.should_download = 1 + else: + mediafile.should_download = 0 diff --git a/kontor-api/src/schema/tysc/__init__.py b/kontor-api/src/schema/tysc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/schema/tysc/sport.py b/kontor-api/src/schema/tysc/sport.py new file mode 100644 index 0000000..8ddbfb3 --- /dev/null +++ b/kontor-api/src/schema/tysc/sport.py @@ -0,0 +1,8 @@ +from uuid import UUID + +from pydantic import BaseModel + + +class SportResponse(BaseModel): + id: UUID + name: str diff --git a/kontor-api/src/static/images/cross.png b/kontor-api/src/static/images/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 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 U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 diff --git a/kontor-api/src/static/js/autocomplete.js b/kontor-api/src/static/js/autocomplete.js new file mode 100644 index 0000000..efd5e90 --- /dev/null +++ b/kontor-api/src/static/js/autocomplete.js @@ -0,0 +1,5 @@ +$( function() { + $( "#autocomplete" ).autocomplete({ + source: "/jobs/autocomplete" + }); + } ); diff --git a/kontor-api/src/templates/comic/artist_detail.html b/kontor-api/src/templates/comic/artist_detail.html new file mode 100644 index 0000000..58ee61b --- /dev/null +++ b/kontor-api/src/templates/comic/artist_detail.html @@ -0,0 +1,50 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Artist Detail +{% endblock %} + +{% block content %} +

+
+
+

Artist Detail

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
Artist Name{{artist.name}}
Works + {% for work in artist.get_comics() %} +

+ {{work}}: +

    + {% for comic in artist.get_comics()[work] %} +
  • {{comic.title}}
  • + {% endfor %} +
+

+ {% endfor %} +
Data Created{{artist.created_date}}
Data Modified{{artist.last_modified_date}}
+
+
+{% endblock %} diff --git a/kontor-api/src/templates/comic/artists.html b/kontor-api/src/templates/comic/artists.html new file mode 100644 index 0000000..466364a --- /dev/null +++ b/kontor-api/src/templates/comic/artists.html @@ -0,0 +1,32 @@ +{% extends "shared/base.html" %} + +{% block title %} + Comic Artists +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+
+
+

Find Artists..

+
+
+
+ {% for artist in artists %} +
+ {% with obj=artist %} + {% include "components/artist_cards.html" %} + {% endwith %} + + {% if loop.index %3 %} +
+ {% else %} +

+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/kontor-api/src/templates/comic/comic_detail.html b/kontor-api/src/templates/comic/comic_detail.html new file mode 100644 index 0000000..7a903e9 --- /dev/null +++ b/kontor-api/src/templates/comic/comic_detail.html @@ -0,0 +1,71 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Comic Detail +{% endblock %} + +{% block content %} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Comic Title{{comic.title}}
Publisher{{comic.publisher.name}}
Completed + {% with check=comic.completed %} + {% include "components/check.html" %} + {% endwith %} +
Works + {% for work in comic.get_artists() %} +

+ {{work}}: +

    + {% for artist in comic.get_artists()[work] %} +
  • {{artist.name}}
  • + {% endfor %} +
+

+ {% endfor %} +
Data Created{{comic.created_date}}
Data Modified{{comic.last_modified_date}}
Data Version{{comic.version}}
Issues + +
+
+
+{% endblock %} diff --git a/kontor-api/src/templates/comic/comics.html b/kontor-api/src/templates/comic/comics.html new file mode 100644 index 0000000..91b6513 --- /dev/null +++ b/kontor-api/src/templates/comic/comics.html @@ -0,0 +1,31 @@ +{% extends "shared/base.html" %} + +{% block title %} + Comic List +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+
+
+

Find Jobs..

+
+
+
+ {% for comic in comics %} +
+ {% with obj=comic %} + {% include "components/comic_cards.html" %} + {% endwith %} + {% if loop.index %3 %} +
+ {% else %} +

+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/kontor-api/src/templates/comic/publisher_detail.html b/kontor-api/src/templates/comic/publisher_detail.html new file mode 100644 index 0000000..6429d58 --- /dev/null +++ b/kontor-api/src/templates/comic/publisher_detail.html @@ -0,0 +1,45 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Publisher Detail +{% endblock %} + +{% block content %} +
+
+
+

Publisher Detail

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
Publisher Name{{publisher.name}}
Comics + +
Data Created{{publisher.created_date}}
Data Modified{{publisher.last_modified_date}}
+
+
+{% endblock %} diff --git a/kontor-api/src/templates/comic/publishers.html b/kontor-api/src/templates/comic/publishers.html new file mode 100644 index 0000000..3eaedc8 --- /dev/null +++ b/kontor-api/src/templates/comic/publishers.html @@ -0,0 +1,31 @@ +{% extends "shared/base.html" %} + +{% block title %} + Publisher List +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+
+
+

Find Publishers..

+
+
+
+ {% for publisher in publishers %} +
+ {% with obj=publisher %} + {% include "components/publisher_cards.html" %} + {% endwith %} + {% if loop.index %3 %} +
+ {% else %} +

+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/kontor-api/src/templates/components/actor_cards.html b/kontor-api/src/templates/components/actor_cards.html new file mode 100644 index 0000000..8678b0e --- /dev/null +++ b/kontor-api/src/templates/components/actor_cards.html @@ -0,0 +1,6 @@ +
+
+
{{obj.name}}
+ Read more +
+
diff --git a/kontor-api/src/templates/components/alerts.html b/kontor-api/src/templates/components/alerts.html new file mode 100644 index 0000000..667fbdc --- /dev/null +++ b/kontor-api/src/templates/components/alerts.html @@ -0,0 +1,5 @@ +{% if msg %} + +{% endif %} diff --git a/kontor-api/src/templates/components/artist_cards.html b/kontor-api/src/templates/components/artist_cards.html new file mode 100644 index 0000000..53192af --- /dev/null +++ b/kontor-api/src/templates/components/artist_cards.html @@ -0,0 +1,6 @@ +
+
+
{{obj.name}}
+ Read more +
+
diff --git a/kontor-api/src/templates/components/check.html b/kontor-api/src/templates/components/check.html new file mode 100644 index 0000000..b2b6263 --- /dev/null +++ b/kontor-api/src/templates/components/check.html @@ -0,0 +1,5 @@ +{% if check == 1 %} + +{% else %} + +{% endif %} diff --git a/kontor-api/src/templates/components/comic_cards.html b/kontor-api/src/templates/components/comic_cards.html new file mode 100644 index 0000000..2a1a5fc --- /dev/null +++ b/kontor-api/src/templates/components/comic_cards.html @@ -0,0 +1,8 @@ +
+
+
{{obj.title}}
+

Publisher : {{obj.publisher.name}}

+

Completed : {% with check=obj.completed %}{% include "components/check.html" %}{% endwith %}

+ Read more +
+
diff --git a/kontor-api/src/templates/components/navbar.html b/kontor-api/src/templates/components/navbar.html new file mode 100644 index 0000000..3d4f974 --- /dev/null +++ b/kontor-api/src/templates/components/navbar.html @@ -0,0 +1,66 @@ + diff --git a/kontor-api/src/templates/components/publisher_cards.html b/kontor-api/src/templates/components/publisher_cards.html new file mode 100644 index 0000000..e2a6050 --- /dev/null +++ b/kontor-api/src/templates/components/publisher_cards.html @@ -0,0 +1,6 @@ +
+
+
{{obj.name}}
+ Read more +
+
diff --git a/kontor-api/src/templates/index.html b/kontor-api/src/templates/index.html new file mode 100644 index 0000000..315ea33 --- /dev/null +++ b/kontor-api/src/templates/index.html @@ -0,0 +1,14 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Kontor +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+
+{% endblock %} diff --git a/kontor-api/src/templates/media/actor_detail.html b/kontor-api/src/templates/media/actor_detail.html new file mode 100644 index 0000000..f163f73 --- /dev/null +++ b/kontor-api/src/templates/media/actor_detail.html @@ -0,0 +1,45 @@ +{% extends "shared/base.html" %} + + +{% block title %} + Actor Detail +{% endblock %} + +{% block content %} +
+
+
+

Actor Detail

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
Actor Name{{actor.name}}
Works + +
Data Created{{actor.created_date}}
Data Modified{{actor.last_modified_date}}
+
+
+{% endblock %} diff --git a/kontor-api/src/templates/media/actors.html b/kontor-api/src/templates/media/actors.html new file mode 100644 index 0000000..836d614 --- /dev/null +++ b/kontor-api/src/templates/media/actors.html @@ -0,0 +1,32 @@ +{% extends "shared/base.html" %} + +{% block title %} + MediaFile Actors +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+
+
+

Actors..

+
+
+
+ {% for actor in actors %} +
+ {% with obj=actor %} + {% include "components/actor_cards.html" %} + {% endwith %} + + {% if loop.index %3 %} +
+ {% else %} +

+ {% endif %} + {% endfor %} +
+ +{% endblock %} diff --git a/kontor-api/src/templates/media/file_detail.html b/kontor-api/src/templates/media/file_detail.html new file mode 100644 index 0000000..186444b --- /dev/null +++ b/kontor-api/src/templates/media/file_detail.html @@ -0,0 +1,65 @@ +{% extends "shared/base.html" %} + + +{% block title %} + MediaFile Detail +{% endblock %} + +{% block content %} +
+
+
+

MediaFile Detail

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MediaFile Title{{mediafile.title}}
MediaFile URL{{mediafile.url}}
MediaFile Cloudlink{{mediafile.cloud_link}}
MediaFile Download?{{mediafile.should_download}}
MediaFile Review?{{mediafile.review}}
Actors + +
Data Created{{mediafile.created_date}}
Data Modified{{mediafile.last_modified_date}}
Data Version{{mediafile.version}}
+
+
+{% endblock %} diff --git a/kontor-api/src/templates/media/files.html b/kontor-api/src/templates/media/files.html new file mode 100644 index 0000000..3fb77a8 --- /dev/null +++ b/kontor-api/src/templates/media/files.html @@ -0,0 +1,29 @@ +{% extends "shared/base.html" %} + +{% block title %} + MediaFiles List +{% endblock %} + +{% block content %} + {% with msg=msg %} + {% include "components/alerts.html" %} + {% endwith %} +
+ + + + + + + + {% for mediafile in mediafiles %} + + + + + + {% endfor %} + +
TitelURLCloudlink
{{mediafile.title}}{{mediafile.url}}{{mediafile.cloud_link}}
+
+{% endblock %} diff --git a/kontor-api/src/templates/shared/base.html b/kontor-api/src/templates/shared/base.html new file mode 100644 index 0000000..af6e986 --- /dev/null +++ b/kontor-api/src/templates/shared/base.html @@ -0,0 +1,26 @@ + + + + + + + + {% block title %} + {% endblock %} + + + + {% include "components/navbar.html" %} + {% block content %} + {% endblock %} + + + + + + {% block scripts %} + {% endblock %} + + + + diff --git a/kontor-api/src/webapps/__init__.py b/kontor-api/src/webapps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/webapps/base.py b/kontor-api/src/webapps/base.py new file mode 100644 index 0000000..24b16cf --- /dev/null +++ b/kontor-api/src/webapps/base.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Request +from fastapi.templating import Jinja2Templates + +from src.webapps.comic import route_comics +from src.webapps.media import route_media + +templates = Jinja2Templates(directory="src/templates") + +api_router = APIRouter() +api_router.include_router(route_comics.router) +api_router.include_router(route_media.router) + +@api_router.get("/") +def home(request: Request, msg: str = None): + return templates.TemplateResponse("index.html", {"request": request, "msg": msg}) diff --git a/kontor-api/src/webapps/comic/__init__.py b/kontor-api/src/webapps/comic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/src/webapps/comic/route_comics.py b/kontor-api/src/webapps/comic/route_comics.py new file mode 100644 index 0000000..565bd26 --- /dev/null +++ b/kontor-api/src/webapps/comic/route_comics.py @@ -0,0 +1,42 @@ +from uuid import UUID + +from fastapi import APIRouter, Request +from fastapi.templating import Jinja2Templates + +from src.apis.utils import SessionDep +from src.db.models.comic import Comic, Artist, Publisher + +templates = Jinja2Templates(directory="src/templates") +router = APIRouter(include_in_schema=False, prefix="/comic") + +@router.get("/comics") +def get_comics(db: SessionDep, request: Request, msg: str = None): + comics = db.query(Comic).all() + return templates.TemplateResponse("comic/comics.html", {"request": request, "msg": msg, "comics": comics}) + +@router.get("/comics/{comic_id}") +def comic_details(comic_id: UUID, request: Request, db: SessionDep): + comic = db.get(Comic, comic_id) + return templates.TemplateResponse("comic/comic_detail.html", {"request": request, "comic":comic}) + +@router.get("/artists") +def get_artists(db: SessionDep, request: Request, msg: str = None): + artists = db.query(Artist).all() + return templates.TemplateResponse("comic/artists.html", {"request": request, "msg": msg, "artists": artists}) + +@router.get("/artists/{artist_id}") +def artist_detail(artist_id: UUID, request: Request, db: SessionDep): + artist = db.get(Artist, artist_id) + return templates.TemplateResponse("comic/artist_detail.html", {"request": request, "artist": artist}) + +@router.get("/publishers") +def get_publishers(db: SessionDep, request: Request, msg: str = None): + publishers = db.query(Publisher).all() + return templates.TemplateResponse("comic/publishers.html", {"request": request, "publishers": publishers}) + +@router.get("/publishers/{publisher_id}") +def publisher_details(publisher_id: UUID, request: Request, db: SessionDep, msg: str = None): + publisher = db.get(Publisher, publisher_id) + if publisher is None: + msg = "Could not find Publisher" + return templates.TemplateResponse("comic/publisher_detail.html", {"request": request, "msg": msg, "publisher": publisher}) diff --git a/kontor-api/src/webapps/media/route_media.py b/kontor-api/src/webapps/media/route_media.py new file mode 100644 index 0000000..b957f49 --- /dev/null +++ b/kontor-api/src/webapps/media/route_media.py @@ -0,0 +1,32 @@ +from uuid import UUID + +from fastapi import APIRouter, Request +from fastapi.templating import Jinja2Templates + +from src.apis.utils import SessionDep +from src.db.models.media import MediaFile, MediaActor +#ifrom src.schema.media.comic import get_comic_details + +templates = Jinja2Templates(directory="src/templates") +router = APIRouter(include_in_schema=False, prefix="/media") + +@router.get("/files") +def get_mediafiles(db: SessionDep, request: Request, msg: str = None): + mediafiles = db.query(MediaFile).all() + return templates.TemplateResponse("media/files.html", {"request": request, "msg": msg, "mediafiles": mediafiles}) + +@router.get("/files/{file_id}") +def file_details(file_id: UUID, request: Request, db: SessionDep): + mediafile = db.get(MediaFile, file_id) + return templates.TemplateResponse("media/file_detail.html", {"request": request, "mediafile":mediafile}) + +@router.get("/actors") +def get_actors(db: SessionDep, request: Request, msg: str = None): + actors = db.query(MediaActor).all() + return templates.TemplateResponse("media/actors.html", {"request": request, "msg": msg, "actors": actors}) + +@router.get("/actors/{actor_id}") +def artist_detail(actor_id: UUID, request: Request, db: SessionDep): + actor = db.get(MediaActor, actor_id) + return templates.TemplateResponse("media/actor_detail.html", {"request": request, "actor": actor}) + diff --git a/kontor-api/tests/__init__.py b/kontor-api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-api/tests/test_main.py b/kontor-api/tests/test_main.py new file mode 100644 index 0000000..850d509 --- /dev/null +++ b/kontor-api/tests/test_main.py @@ -0,0 +1,15 @@ +from fastapi.testclient import TestClient +import pytest +from src.main import app + + +@pytest.fixture(name="client") +def client_fixture(): + client = TestClient(app) + yield client + + +def test_get_artists(client: TestClient): + response = client.get("/comic/artists") + assert response.status_code == 200 + assert len(response.json()) == 5 diff --git a/kontor-api/uv.lock b/kontor-api/uv.lock new file mode 100644 index 0000000..f12445d --- /dev/null +++ b/kontor-api/uv.lock @@ -0,0 +1,811 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload_time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload_time = "2025-01-31T02:16:47.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload_time = "2025-01-31T02:16:45.015Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload_time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload_time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload_time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload_time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload_time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload_time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload_time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload_time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload_time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload_time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload_time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload_time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload_time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload_time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload_time = "2024-12-24T18:12:32.852Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload_time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload_time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "ecdsa" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload_time = "2025-03-13T11:52:43.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload_time = "2025-03-13T11:52:41.757Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload_time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload_time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload_time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload_time = "2025-03-23T22:55:42.101Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753, upload_time = "2024-12-15T14:28:10.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705, upload_time = "2024-12-15T14:28:06.18Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "greenlet" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685, upload_time = "2025-04-15T16:21:26.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131, upload_time = "2025-04-15T16:19:19.469Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323, upload_time = "2025-04-15T16:49:02.677Z" }, + { url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430, upload_time = "2025-04-15T16:50:43.445Z" }, + { url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798, upload_time = "2025-04-15T16:55:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271, upload_time = "2025-04-15T16:22:42.458Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779, upload_time = "2025-04-15T16:22:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968, upload_time = "2025-04-15T16:52:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510, upload_time = "2025-04-15T16:23:01.873Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004, upload_time = "2025-04-15T16:55:46.007Z" }, + { url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900, upload_time = "2025-04-15T16:49:04.099Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270, upload_time = "2025-04-15T16:50:44.769Z" }, + { url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534, upload_time = "2025-04-15T16:55:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826, upload_time = "2025-04-15T16:22:44.545Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697, upload_time = "2025-04-15T16:22:43.796Z" }, + { url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762, upload_time = "2025-04-15T16:52:55.245Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173, upload_time = "2025-04-15T16:23:03.009Z" }, + { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908, upload_time = "2025-04-15T16:20:33.58Z" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload_time = "2022-09-25T15:40:01.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload_time = "2022-09-25T15:39:59.68Z" }, +] + +[[package]] +name = "httpcore" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "h11" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/ad/c98ecdbfe04417e71e143bf2f2fb29128e4787d78d1cedba21bd250c7e7a/httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", size = 62676, upload_time = "2023-07-05T12:09:31.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/2c/2bde7ff8dd2064395555220cbf7cba79991172bf5315a07eb3ac7688d9f1/httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87", size = 74513, upload_time = "2023-07-05T12:09:29.425Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload_time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload_time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload_time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload_time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload_time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload_time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload_time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload_time = "2024-10-16T19:44:46.46Z" }, +] + +[[package]] +name = "httpx" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/2a/114d454cb77657dbf6a293e69390b96318930ace9cd96b51b99682493276/httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd", size = 81858, upload_time = "2023-05-19T00:50:56.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/91/e41f64f03d2a13aee7e8c819d82ee3aa7cdc484d18c0ae859742597d5aa0/httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", size = 75377, upload_time = "2023-05-19T00:50:54.91Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload_time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload_time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload_time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kontor-api" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "fastapi", extra = ["standard"] }, + { name = "httpx" }, + { name = "mariadb" }, + { name = "natsort" }, + { name = "pathlib" }, + { name = "platformdirs" }, + { name = "pytest" }, + { name = "python-dotenv" }, + { name = "python-jose" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "sqlmodel" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, + { name = "httpx", specifier = "==0.24.1" }, + { name = "mariadb", specifier = ">=1.1.12" }, + { name = "natsort", specifier = ">=8.4.0" }, + { name = "pathlib", specifier = ">=1.0.1" }, + { name = "platformdirs", specifier = ">=4.3.7" }, + { name = "pytest", specifier = "==7.4.0" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "python-jose", specifier = ">=3.4.0" }, + { name = "python-multipart", specifier = ">=0.0.20" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, + { name = "sqlmodel", specifier = ">=0.0.24" }, +] + +[[package]] +name = "mariadb" +version = "1.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/bb/4bbc803fbdafedbfba015f7cc1ab1e87a6d1de36725ba058c53e2f8a45ad/mariadb-1.1.12.tar.gz", hash = "sha256:50b02ff2c78b1b4f4628a054e3c8c7dd92972137727a5cc309a64c9ed20c878c", size = 85934, upload_time = "2025-02-13T13:11:48.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/1b/b6eca3870ac1b5577a10d3b49ba42ac263c2e5718c9224cc1c8463940422/mariadb-1.1.12-cp313-cp313-win32.whl", hash = "sha256:ba43c42130d41352f32a5786c339cc931d05472ef7640fa3764d428dc294b88e", size = 184338, upload_time = "2025-02-13T13:11:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ff/c29a543ee1f9009755bc304138f61cd9b0ee1f14533e446513f84ccf143a/mariadb-1.1.12-cp313-cp313-win_amd64.whl", hash = "sha256:b69bc18418e72fcf359d17736cdc3f601a271203aff13ef7c57a415c8fd52ab0", size = 201272, upload_time = "2025-02-13T13:11:38.074Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload_time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload_time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload_time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload_time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload_time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload_time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload_time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload_time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload_time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload_time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload_time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload_time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload_time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload_time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload_time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload_time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload_time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload_time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload_time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload_time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "natsort" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575, upload_time = "2023-06-20T04:17:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268, upload_time = "2023-06-20T04:17:17.522Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload_time = "2014-09-03T15:41:57.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload_time = "2022-05-04T13:37:20.585Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload_time = "2025-03-19T20:36:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload_time = "2025-03-19T20:36:09.038Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload_time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload_time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/db/fffec68299e6d7bad3d504147f9094830b704527a7fc098b721d38cc7fa7/pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", size = 146820, upload_time = "2019-11-16T17:27:38.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/1e/a94a8d635fa3ce4cfc7f506003548d0a2447ae76fd5ca53932970fe3053f/pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", size = 77145, upload_time = "2019-11-16T17:27:11.07Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload_time = "2025-04-08T13:27:06.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload_time = "2025-04-08T13:27:03.789Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload_time = "2025-04-02T09:49:41.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload_time = "2025-04-02T09:47:51.648Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload_time = "2025-04-02T09:47:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload_time = "2025-04-02T09:47:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload_time = "2025-04-02T09:47:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload_time = "2025-04-02T09:47:58.088Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload_time = "2025-04-02T09:47:59.591Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload_time = "2025-04-02T09:48:01.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload_time = "2025-04-02T09:48:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload_time = "2025-04-02T09:48:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload_time = "2025-04-02T09:48:06.226Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload_time = "2025-04-02T09:48:08.114Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262, upload_time = "2025-04-02T09:48:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626, upload_time = "2025-04-02T09:48:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590, upload_time = "2025-04-02T09:48:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload_time = "2025-04-02T09:48:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload_time = "2025-04-02T09:48:16.222Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810, upload_time = "2025-04-02T09:48:17.97Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/f3/dadfbdbf6b6c8b5bd02adb1e08bc9fbb45ba51c68b0893fa536378cdf485/pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a", size = 1349733, upload_time = "2023-06-23T11:17:28.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/b2/741130cbcf2bbfa852ed95a60dc311c9e232c7ed25bac3d9b8880a8df4ae/pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", size = 323580, upload_time = "2023-06-23T11:17:25.738Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload_time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload_time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-jose" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ecdsa" }, + { name = "pyasn1" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/a0/c49687cf40cb6128ea4e0559855aff92cd5ebd1a60a31c08526818c0e51e/python-jose-3.4.0.tar.gz", hash = "sha256:9a9a40f418ced8ecaf7e3b28d69887ceaa76adad3bcaa6dae0d9e596fec1d680", size = 92145, upload_time = "2025-02-18T17:26:41.985Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/b0/2586ea6b6fd57a994ece0b56418cbe93fff0efb85e2c9eb6b0caf24a4e37/python_jose-3.4.0-py2.py3-none-any.whl", hash = "sha256:9c9f616819652d109bd889ecd1e15e9a162b9b94d682534c9c2146092945b78f", size = 34616, upload_time = "2025-02-18T17:26:40.826Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload_time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload_time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload_time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload_time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload_time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload_time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload_time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload_time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload_time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload_time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/ea/13945d58d556a28dfb0f774ad5c8af759527390e59505a40d164bf8ce1ce/rich_toolkit-0.14.1.tar.gz", hash = "sha256:9248e2d087bfc01f3e4c5c8987e05f7fa744d00dd22fa2be3aa6e50255790b3f", size = 104416, upload_time = "2025-03-30T12:19:08.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/e8/61c5b12d1567fdba41a6775db12a090d88b8305424ee7c47259c70d33cb4/rich_toolkit-0.14.1-py3-none-any.whl", hash = "sha256:dc92c0117d752446d04fdc828dbca5873bcded213a091a5d3742a2beec2e6559", size = 24177, upload_time = "2025-03-30T12:19:07.307Z" }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload_time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload_time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload_time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload_time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload_time = "2025-03-27T17:52:31.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload_time = "2025-03-27T18:40:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload_time = "2025-03-27T18:40:07.182Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload_time = "2025-03-27T18:51:29.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131, upload_time = "2025-03-27T18:50:31.616Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364, upload_time = "2025-03-27T18:51:31.336Z" }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482, upload_time = "2025-03-27T18:50:33.201Z" }, + { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704, upload_time = "2025-03-27T18:46:00.193Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564, upload_time = "2025-03-27T18:46:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload_time = "2025-03-27T18:40:43.796Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload_time = "2025-03-07T05:43:32.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload_time = "2025-03-07T05:43:30.37Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload_time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload_time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload_time = "2025-02-27T19:17:34.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload_time = "2025-02-27T19:17:32.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload_time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload_time = "2025-02-25T17:27:57.754Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload_time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload_time = "2025-04-19T06:02:48.42Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload_time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload_time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload_time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload_time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload_time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload_time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload_time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload_time = "2025-04-08T10:36:26.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531, upload_time = "2025-04-08T10:35:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417, upload_time = "2025-04-08T10:35:37.048Z" }, + { url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423, upload_time = "2025-04-08T10:35:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185, upload_time = "2025-04-08T10:35:39.708Z" }, + { url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696, upload_time = "2025-04-08T10:35:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327, upload_time = "2025-04-08T10:35:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741, upload_time = "2025-04-08T10:35:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995, upload_time = "2025-04-08T10:35:46.336Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693, upload_time = "2025-04-08T10:35:48.161Z" }, + { url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677, upload_time = "2025-04-08T10:35:49.65Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804, upload_time = "2025-04-08T10:35:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload_time = "2025-04-08T10:35:52.458Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload_time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload_time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload_time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload_time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload_time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload_time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload_time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload_time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload_time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload_time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload_time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload_time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload_time = "2025-03-05T20:03:39.41Z" }, +] diff --git a/kontor-gui/.gitignore b/kontor-gui/.gitignore new file mode 100644 index 0000000..e877d19 --- /dev/null +++ b/kontor-gui/.gitignore @@ -0,0 +1,9 @@ +deployment/ +venv/ +kontor.bin +bin/ +include/ +lib/ +lib64/ +lib64 +env/ diff --git a/kontor-gui/Makefile b/kontor-gui/Makefile new file mode 100644 index 0000000..5bb3fcd --- /dev/null +++ b/kontor-gui/Makefile @@ -0,0 +1,31 @@ +.PHONY: clean virtualenv test docker dist dist-upload + +clean: + find . -name '*.py[co]' -delete + +virtualenv: + virtualenv --prompt '|> kontor <| ' env + env/bin/pip install -r requirements.txt + env/bin/python setup.py develop + @echo + @echo "VirtualENV Setup Complete. Now run: source env/bin/activate" + @echo + +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +docker: clean + docker build -t kontor-api:latest . + +dist: clean + rm -rf dist/* + python setup.py sdist + python setup.py bdist_wheel + +dist-upload: + twine upload dist/* diff --git a/kontor-gui/README.md b/kontor-gui/README.md new file mode 100644 index 0000000..c2c7bec --- /dev/null +++ b/kontor-gui/README.md @@ -0,0 +1 @@ +# kontor-gui diff --git a/kontor-gui/gui/__init__.py b/kontor-gui/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-gui/gui/comic_window.py b/kontor-gui/gui/comic_window.py new file mode 100644 index 0000000..d032998 --- /dev/null +++ b/kontor-gui/gui/comic_window.py @@ -0,0 +1,66 @@ +from PySide6.QtCore import Signal, QSortFilterProxyModel +from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QTableView, QMdiSubWindow, \ + QHeaderView + +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel + + +class ComicWindow(QMdiSubWindow): + closed = Signal() + + def __init__(self, main_window): + super().__init__() + self.data_views = list() + self._main_window = main_window + self.log = main_window.log + self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross + + def _init_gui(self): + self.setWindowTitle("Comics") + self.setWidget(QWidget()) + layout = QVBoxLayout() + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("comic"), "Comics") + self.tabs.addTab(self.generate_data_tab("publisher"), "Publisher") + self.tabs.currentChanged.connect(self._tab_changed) + layout.addWidget(self.tabs) + self.setLayout(layout) + self.setWidget(self.tabs) + + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('comic') + + def refresh(self): + # self.log.info("refresh") + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def update_status(self, message): + self._main_window.update_status(message) + + def generate_data_tab(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + header = table_view.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + table_view.setModel(proxy_model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + return data_tab diff --git a/kontor-gui/gui/data_view.py b/kontor-gui/gui/data_view.py new file mode 100644 index 0000000..91c66d2 --- /dev/null +++ b/kontor-gui/gui/data_view.py @@ -0,0 +1,12 @@ +from abc import ABC, abstractmethod + + +class DataViewMeta(ABC): + @abstractmethod + def get_header(self): + pass + + +class ComicView(DataViewMeta): + def get_header(self): + pass diff --git a/kontor-gui/gui/data_view_model.py b/kontor-gui/gui/data_view_model.py new file mode 100644 index 0000000..4ffdc8c --- /dev/null +++ b/kontor-gui/gui/data_view_model.py @@ -0,0 +1,32 @@ +from typing import List + +from PySide6.QtCore import QModelIndex, QAbstractTableModel +from PySide6.QtGui import Qt + +from gui.data_view import DataViewMeta + + +class DataViewModel(QAbstractTableModel): + def __init__(self): + super().__init__() + self.main_window = None + self._config = None + self._data = List[DataViewMeta] + + def rowCount(self, parent=QModelIndex()): + return len(self._data) + + def columnCount(self, parent=QModelIndex()): + return 0 + + def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): + return None + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + return None + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + return False + + def flags(self, index): + return None diff --git a/kontor-gui/gui/dialogs.py b/kontor-gui/gui/dialogs.py new file mode 100644 index 0000000..21dbe92 --- /dev/null +++ b/kontor-gui/gui/dialogs.py @@ -0,0 +1,106 @@ +from pathlib import Path + +from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QFileDialog, \ + QCheckBox, QComboBox + + +class ExportKontorDialog(QDialog): + def __init__(self, parent=None, kontor_db=None): + super().__init__(parent) + + self.parent = parent + self.kontor_db = kontor_db + self.file_name = "data.json" + self.tables = [] + self._table_options = {} + + self.export_options = {"JSON": {"ext": ".json"}, "YAML": {"ext": ".yaml"}, "SQLite": {"ext": ".db"}} + self.current_export_type = "JSON" + + buttons = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(buttons) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + layout = QVBoxLayout() + + self.label = QLabel() + self.label.setText("Export DB to data.json") + + self.combo_box = QComboBox() + self.combo_box.addItems(["JSON", "YAML", "SQLite"]) + self.combo_box.currentTextChanged.connect(self.change_export_type) + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_layout.addWidget(self.combo_box) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + + for table_name in self.kontor_db.get_table_names(): + check_box = QCheckBox(table_name) + check_box.setChecked(True) + self.tables.append(table_name) + self._table_options[table_name] = check_box + check_box.stateChanged.connect(self.change_selection) + layout.addWidget(check_box) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def change_selection(self): + self.tables.clear() + for (name, box) in self._table_options.items(): + if box.isChecked(): + self.tables.append(name) + + def change_export_type(self, text): + self.current_export_type = text + self.label.setText(f'Export DB to data.{self.export_options[text]["ext"]}') + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.AnyFile) + file_dialog.setDefaultSuffix(self.export_options[self.current_export_type]["ext"]) + file_dialog.setNameFilter(f'*{self.export_options[self.current_export_type]["ext"]}') + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + export_file = Path(self.file_name) + self.file_name = export_file.with_suffix(self.export_options[self.current_export_type]["ext"]) + self.label.setText(f"Export DB to {self.file_name}") + + def get_tables_to_export(self) -> list: + return self.tables + + +class ImportKontorDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.file_name = None + + QBtn = (QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + + self.buttonBox = QDialogButtonBox(QBtn) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + self.label = QLabel() + self.label.setText("Import DB from data.json") + layout = QVBoxLayout() + file_layout = QHBoxLayout() + file_layout.addWidget(self.label) + file_button = QPushButton("Select file") + file_button.clicked.connect(self.select_file) + file_layout.addWidget(file_button) + layout.addLayout(file_layout) + layout.addWidget(self.buttonBox) + self.setLayout(layout) + + def select_file(self): + file_dialog = QFileDialog() + file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile) + if file_dialog.exec(): + self.file_name = file_dialog.selectedFiles()[0] + self.label.setText(f"Import DB from {self.file_name}") diff --git a/kontor-gui/gui/main_window.py b/kontor-gui/gui/main_window.py new file mode 100644 index 0000000..336e2be --- /dev/null +++ b/kontor-gui/gui/main_window.py @@ -0,0 +1,237 @@ +from PySide6.QtCore import Qt +from PySide6.QtGui import QAction, QIcon, QGuiApplication +from PySide6.QtWidgets import QMenu, QMessageBox, QProgressBar, QMdiArea +from PySide6.QtWidgets import QLabel, QMainWindow +from sqlalchemy import Engine +from kontor_schema import KontorDB + +from .comic_window import ComicWindow +from .media_window import MediaWindow +from .meta_data_window import MetaDataWindow +from .progress import ProgressUpdate +from .dialogs import ExportKontorDialog, ImportKontorDialog +from .worker import VideoDownloader + + +class MainWindow(QMainWindow): + + def __init__(self, engine: Engine, log): + super().__init__() + + self.downloader = None + self.tick = QIcon('res/tick.png') + self.cross = QIcon('res/cross.png') + self.import_icon = QIcon("res/application-import.png") + self.export_icon = QIcon("res/application-export.png") + self.circle_icon = QIcon("res/arrow-circle-double.png") + + self.data = [] + self.filter = {} + self.kontor_db = KontorDB(engine, log) + self.log = log + self._subwindows = {} + self.media_dir = "/data/media" + self.dl_tool = "yt-dlp" + + self._setup_ui() + + def _setup_ui(self): + self.setWindowTitle("Kontor") + self.setMinimumSize(1200, 800) + self._create_actions() + self.mdi_area = QMdiArea() + self.setCentralWidget(self.mdi_area) + self.mdi_area.setObjectName('mdi_area') + self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self._create_menubar() + self._create_toolbars() + self.status_progress = QProgressBar() + self.progress_update = ProgressUpdate(self.status_progress) + self._create_statusbar() + center_point = QGuiApplication.screens()[0].geometry().center() + self.move(center_point - self.frameGeometry().center()) + + def _create_actions(self): + self.newAction = QAction("&New", self) + self.aboutAction = QAction("&Über...", self) + self.aboutAction.triggered.connect(self.about) + self.showComicWindow = QAction("&Comic Window", self) + self.showComicWindow.triggered.connect(self.show_comic_window) + self.showTyscWindow = QAction("TYSC Window", self) + self.showMediaWindow = QAction("&Media Window", self) + self.showMediaWindow.triggered.connect(self.show_media_window) + self.showMetaDataWindow = QAction("Meta Data Window", self) + self.showMetaDataWindow.triggered.connect(self.show_meta_data_window) + self.importAction = QAction(self.import_icon, "&Import", self) + self.importAction.triggered.connect(self.import_from_file) + self.exportAction = QAction(self.export_icon, "&Export", self) + self.exportAction.triggered.connect(self.export_to_file) + self.refreshAction = QAction(self.circle_icon, "&Refresh", self) + self.refreshAction.triggered.connect(self.refresh) + self.updateTitleAction = QAction("&Update Titles", self) + self.updateTitleAction.triggered.connect(self.update_title) + self.downloadAction = QAction("&Download Videos", self) + self.downloadAction.triggered.connect(self.start_download) + self.checkFileAction = QAction("&Check files", self) + self.checkFileAction.triggered.connect(self.check_files) + self.exitAction = QAction("&Beenden", self) + self.exitAction.setShortcut("Alt+F4") + self.exitAction.triggered.connect(self.close) + + def _create_menubar(self): + menu_bar = self.menuBar() + # File menu + file_menu = QMenu("&Datei") + menu_bar.addMenu(file_menu) + file_menu.addAction(self.exitAction) + # Kontor menu + kontor_menu = QMenu("&Kontor") + menu_bar.addMenu(kontor_menu) + kontor_menu.addAction(self.importAction) + kontor_menu.addAction(self.exportAction) + comic_menu = QMenu("&Comic") + comic_menu.addAction(self.showComicWindow) + tysc_menu = QMenu("&TradeYourSportCards") + media_file_menu = QMenu("&MediaFile") + media_file_menu.addAction(self.updateTitleAction) + media_file_menu.addAction(self.downloadAction) + media_file_menu.addAction(self.checkFileAction) + kontor_menu.addMenu(comic_menu) + kontor_menu.addMenu(tysc_menu) + kontor_menu.addMenu(media_file_menu) + window_menu = QMenu("&Window") + layouts_menu = QMenu("&Layouts") + window_menu.addMenu(layouts_menu) + window_menu.addAction(self.showComicWindow) + window_menu.addAction(self.showMediaWindow) + window_menu.addAction(self.showMetaDataWindow) + menu_bar.addMenu(window_menu) + # Help menu + help_menu = QMenu("&Hilfe") + menu_bar.addMenu(help_menu) + help_menu.addAction(self.aboutAction) + + def _create_toolbars(self): + # Kontor toolbar + kontor_tool_bar = self.addToolBar("Kontor") + kontor_tool_bar.addAction(self.importAction) + kontor_tool_bar.addAction(self.exportAction) + kontor_tool_bar.addAction(self.refreshAction) + + def _create_statusbar(self): + self.statusBar = self.statusBar() + self.statusBar.showMessage("Kontor ready", 6000) + self.status_label = QLabel("") + self.status_progress.setEnabled(False) + self.statusBar.addPermanentWidget(self.status_progress) + + def about(self): + QMessageBox.about(self, "Über Kontor", "Python: 3.11\nKontor: 0.1.0") + + def show_comic_window(self): + if 'comic' not in self._subwindows: + comic = ComicWindow(self) + comic.closed.connect(self.sub_window_closed) + self._subwindows['comic'] = comic + self.mdi_area.addSubWindow(comic) + comic.show() + else: + comic = self._subwindows.pop('comic') + comic.close() + self.mdi_area.removeSubWindow(comic) + + def show_media_window(self): + if 'media' not in self._subwindows: + media = MediaWindow(self) + media.closed.connect(self.sub_window_closed) + self._subwindows['media'] = media + self.mdi_area.addSubWindow(media) + media.show() + else: + media = self._subwindows.pop('media') + media.close() + self.mdi_area.removeSubWindow(media) + + def show_meta_data_window(self): + if 'meta_data' not in self._subwindows: + meta_data = MetaDataWindow(self) + meta_data.closed.connect(self.sub_window_closed) + self._subwindows['meta_data'] = meta_data + self.mdi_area.addSubWindow(meta_data) + meta_data.show() + else: + meta_data = self._subwindows.pop('meta_data') + meta_data.close() + self.mdi_area.removeSubWindow(meta_data) + + def remove_sub_window(self, name: str): + self.log.info("remove subwindow %s", name) + if name in self._subwindows: + window = self._subwindows.pop(name) + window.close() + self.mdi_area.removeSubWindow(window) + + def sub_window_closed(self): + self.log.info("close subwindow") + + def import_from_file(self): + import_dlg = ImportKontorDialog(self) + if import_dlg.exec(): + print(f"import DB from file {import_dlg.file_name}") + self.kontor_db.import_db(import_dlg.file_name) + else: + print("do nothing for import") + + def export_to_file(self): + export_dlg = ExportKontorDialog(self, self.kontor_db) + if export_dlg.exec(): + self.log.info(export_dlg.get_tables_to_export()) + self.log.info(f"export DB to {export_dlg.file_name}") + self.statusBar.showMessage(f"export DB to {export_dlg.file_name}", 3000) + self.kontor_db.export_db(export_dlg.current_export_type, export_dlg.file_name) + else: + self.statusBar.showMessage("Export cancelled", 3000) + + def update_title(self): + self.log.info("update title for table MediaFile") + self.statusBar.showMessage("update title for table MediaFile", 3000) + self.status_progress.setEnabled(True) + self.kontor_db.update_titles() + self.status_progress.setEnabled(False) + self.refresh() + + def start_download(self): + self.status_progress.setEnabled(True) + self.statusBar.showMessage("download videos for table MediaFile", 3000) + self.downloader = VideoDownloader(self.kontor_db, self.log) + self.downloader.setTotalProgress.connect(self.status_progress.setMaximum) + self.downloader.setCurrentProgress.connect(self.downloadProgress) + self.downloader.succeeded.connect(self.downloadSucceeded) + self.downloader.finished.connect(self.downloadFinished) + self.downloader.start() + + def downloadProgress(self, value: int): + self.status_progress.setValue(value) + self.refresh() + + def downloadSucceeded(self): + self.status_progress.setValue(self.status_progress.maximum()) + self.statusBar.showMessage("Download succeeded", 3000) + + def downloadFinished(self): + self.status_progress.setEnabled(False) + del self.downloader + + def check_files(self): + self.log.info("check files") + self.statusBar.showMessage("check files for table MediaFile", 3000) + self.kontor_db.check_files() + + def refresh(self): + self.log.info("refresh") + for (_, window) in self._subwindows.items(): + window.refresh() + + def update_status(self, message, timeout=3000): + self.statusBar.showMessage(message, timeout=timeout) diff --git a/kontor-gui/gui/media_window.py b/kontor-gui/gui/media_window.py new file mode 100644 index 0000000..f084c76 --- /dev/null +++ b/kontor-gui/gui/media_window.py @@ -0,0 +1,77 @@ +from PySide6.QtCore import Signal, QSortFilterProxyModel +from PySide6.QtWidgets import QMdiSubWindow, QWidget, QVBoxLayout, QTabWidget, QTableView + +from .model_config import KontorModelConfig +from .table_details import KontorTableDetailsView +from .table_model import KontorTableModel + + +class MediaWindow(QMdiSubWindow): + closed = Signal() + + def __init__(self, main_window): + super().__init__() + self.data_views = list() + self._main_window = main_window + self.log = main_window.log + self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross + + def _init_gui(self): + self.setWindowTitle("Media") + self.setWidget(QWidget()) + layout = QVBoxLayout() + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("media_file"), "Media File") + self.tabs.addTab(self.generate_data_tab("media_video"), "Media Video") + self.tabs.addTab(self.generate_data_tab("media_article"), "Media Article") + self.tabs.addTab(self.generate_data_tab_with_details("media_actor"), "Media Actor") + self.tabs.currentChanged.connect(self._tab_changed) + layout.addWidget(self.tabs) + self.setLayout(layout) + self.setWidget(self.tabs) + + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('media') + + def refresh(self): + self.log.info("MediaWindow.refresh") + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def update_status(self, message): + self._main_window.update_status(message) + + def generate_data_tab(self, table_name): + data_tab = QWidget() + + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + table_view.setModel(proxy_model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + table_view.resizeColumnToContents(0) + return data_tab + + def generate_data_tab_with_details(self, table_name): + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + self.data_views.append(model) + details_view = KontorTableDetailsView(model) + return details_view.data_view + + def cell_selected(self, item): + self.log.info(f"Cell {item.row()}:{item.column()} clicked") diff --git a/kontor-gui/gui/meta_data_window.py b/kontor-gui/gui/meta_data_window.py new file mode 100644 index 0000000..567abeb --- /dev/null +++ b/kontor-gui/gui/meta_data_window.py @@ -0,0 +1,66 @@ +from PySide6.QtCore import Signal, QSortFilterProxyModel +from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QTableView, QMdiSubWindow + +from gui.model_config import KontorModelConfig +from gui.table_model import KontorTableModel + + +class MetaDataWindow(QMdiSubWindow): + closed = Signal() + + def __init__(self, main_window): + super().__init__() + self.data_views = list() + self._main_window = main_window + self.log = main_window.log + self._init_gui() + self.tick = main_window.tick + self.cross = main_window.cross + + def _init_gui(self): + self.setWindowTitle("Meta Data") + self.setWidget(QWidget()) + layout = QVBoxLayout() + self.tabs = QTabWidget() + self.tabs.addTab(self.generate_data_tab("module_data"), "Module") + self.tabs.addTab(self.generate_data_tab("meta_data_table"), "Tables") + self.tabs.addTab(self.generate_data_tab("meta_data_column"), "Columns") + self.tabs.currentChanged.connect(self._tab_changed) + layout.addWidget(self.tabs) + self.setLayout(layout) + self.setWidget(self.tabs) + + def closeEvent(self, event): + self.closed.emit() + super().closeEvent(event) + self._main_window.remove_sub_window('meta_data') + + def refresh(self): + # self.log.info("refresh") + self.data_views[self.tabs.currentIndex()].refresh() + + def _tab_changed(self, tab_index): + self.data_views[tab_index].refresh() + + def update_status(self, message): + self._main_window.update_status(message) + + def generate_data_tab(self, table_name): + data_tab = QWidget() + table_config = KontorModelConfig(self._main_window.kontor_db, self, table_name) + model = KontorTableModel(table_config) + layout = QVBoxLayout() + self.data_views.append(model) + data_tab.setLayout(layout) + table_view = QTableView() + # header = table_view.horizontalHeader() + # header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(model) + table_view.setSortingEnabled(True) + table_view.setModel(proxy_model) + layout.addLayout(table_config.get_filter_layout()) + layout.addWidget(table_view) + model.refresh() + table_view.resizeColumnToContents(0) + return data_tab diff --git a/kontor-gui/gui/model_config.py b/kontor-gui/gui/model_config.py new file mode 100644 index 0000000..6a4e00b --- /dev/null +++ b/kontor-gui/gui/model_config.py @@ -0,0 +1,55 @@ +from PySide6.QtWidgets import QHBoxLayout, QCheckBox +from kontor_schema import KontorDB, ColumnEntry + + +class KontorModelConfig: + + def __init__(self, kontor_db: KontorDB, main_window, table_name: str): + self.header = {} + self.filter = {} + self.main_window = main_window + self.log = main_window.log + self._table_name = table_name + self.kontor_db = kontor_db + self.get_table_config() + + def __str__(self): + return f"KontorModelConfig({self._table_name})" + + def get_table_config(self): + # self.log.info("get_table_config %s", self) + self.header = self.kontor_db.get_column_meta_data(self._table_name) + self.filter = self.kontor_db.get_filters(self._table_name) + # self.log.info("headers: %s", self.header) + # self.log.info("%s filters: %s", self, self.filter) + + def filters(self) -> dict: + # self.log.info("%s filters: %s", self, self.filter) + _filters = {} + # print(self.filter["download"].isChecked()) + for column, filter_info in self.filter.items(): + # print(column, filter_info) + if filter_info[ColumnEntry.COLUMN_WIDGET].isChecked(): + _filters[column] = True + # print(f"{filter_rule=}") + # self.log.info("filters -> %s", _filters) + return _filters + + def get_data(self) -> list: + # self.log.info("get_data") + data = self.kontor_db.data(self._table_name, self.header, self.filters()) + # self.log.info("get_data: %d %s", len(data), data) + return data + + def get_filter_layout(self) -> QHBoxLayout: + # self.log.info("get_filter_layout: %s", self.filter) + filter_layout = QHBoxLayout() + for column, filter_info in self.filter.items(): + filter_checkbox = QCheckBox() + filter_checkbox.setText(filter_info[ColumnEntry.COLUMN_LABEL]) + filter_checkbox.checkStateChanged.connect(self.main_window.refresh) + self.filter[column][ColumnEntry.COLUMN_WIDGET] = filter_checkbox + filter_layout.addWidget(filter_checkbox) + filter_layout.addStretch() + # self.log.info("get_filter_layout: %s", self.filter) + return filter_layout diff --git a/kontor-gui/gui/progress.py b/kontor-gui/gui/progress.py new file mode 100644 index 0000000..1b44c26 --- /dev/null +++ b/kontor-gui/gui/progress.py @@ -0,0 +1,18 @@ +from PySide6.QtWidgets import QProgressBar + + +class ProgressUpdate: + def __init__(self, progress: QProgressBar): + self.start = 0 + self.end = 0 + self.current = 0 + self.progress = progress + + def start(self, start_value, end_value): + self.start = start_value + self.end = end_value + self.current = start_value + self.progress.update() + + def update(self, current): + self.progress.update() diff --git a/kontor-gui/gui/table_details.py b/kontor-gui/gui/table_details.py new file mode 100644 index 0000000..07760f0 --- /dev/null +++ b/kontor-gui/gui/table_details.py @@ -0,0 +1,60 @@ +from PySide6.QtCore import QSortFilterProxyModel +from PySide6.QtWidgets import QHBoxLayout, QWidget, QTableView, QVBoxLayout, QFormLayout, QLineEdit, QLabel + +from .table_model import KontorTableModel + + +class KontorTableDetailsView: + def __init__(self, table_model: KontorTableModel): + self._data_view: QWidget = QWidget() + self._model = table_model + self.log = table_model.log + self._table_view = QTableView() + self._label = QLabel() + self.init_gui() + + def init_gui(self): + self.log.info("KontorTableDetailsView.init_gui()") + layout = QVBoxLayout() + self._data_view.setLayout(layout) + details_layout = QHBoxLayout() + table_with_details = QWidget() + table_with_details.setLayout(details_layout) + + self._table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows) + proxy_model = QSortFilterProxyModel() + proxy_model.setSourceModel(self._model) + self._table_view.setSortingEnabled(True) + self._table_view.setModel(proxy_model) + self._table_view.clicked.connect(self.update_details) + self._table_view.activated.connect(self.refresh_details) + + layout.addLayout(self._model.config.get_filter_layout()) + details_layout.addWidget(self._table_view) + + form = QWidget() + form_layout = QFormLayout(form) + form.setLayout(form_layout) + + title = QLineEdit(form) + form_layout.addRow("ID", self._label) + form_layout.addRow("Title", title) + # layout.addWidget(table_view) + details_layout.addWidget(form) + layout.addWidget(table_with_details) + self._model.refresh() + self._table_view.resizeColumnToContents(0) + + @property + def data_view(self): + return self._data_view + + def update_details(self, item): + print(f"Cell {item.row()}-{item.column()} selected") + self.log.info(f"Cell {item.row()}-{item.column()} selected") + self._label.setText(self._model.raw_data()[item.row()][0]) + + def refresh_details(self): + indexes = self._table_view.selectedIndexes() + for index in indexes: + self.log.info(f"refresh_details: Cell {index.row()}-{index.column()} selected") diff --git a/kontor-gui/gui/table_model.py b/kontor-gui/gui/table_model.py new file mode 100644 index 0000000..fb32802 --- /dev/null +++ b/kontor-gui/gui/table_model.py @@ -0,0 +1,128 @@ +from datetime import datetime +from typing import Any + +from PySide6.QtCore import QAbstractTableModel, QModelIndex +from PySide6.QtGui import Qt, QColor +from kontor_schema.database import ColumnEntry + +from .model_config import KontorModelConfig + + +def get_display_value(value: Any, column_config: dict, window) -> str: + if isinstance(value, datetime): + return value.strftime("%Y-%m-%d %M:%M:%S") + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + if value is None: + return "" + # window.log.info(f"unknown type: {column_config[ColumnEntry.COLUMN_TYPE]} - {type(value)}") + return str(value) + + +def get_edit_value(value, column_config, window): + # window.log.info(f"edit value {value}") + return str(value) + + +def get_decoration_value(value: Any, column_config: dict, window): + if column_config[ColumnEntry.COLUMN_TYPE] == 'BOOLEAN': + if value == 1: + return window.tick + else: + return window.cross + + +def get_background_value(value: Any, column_config: dict, window): + if value is None: + return QColor('lightgrey') + + +class KontorTableModel(QAbstractTableModel): + + def __init__(self, model_config: KontorModelConfig): + super().__init__() + self._main_window = model_config.main_window + self._config = model_config + self._data = [] + self.log = model_config.log + + def __str__(self): + return f"KontorTableModel({self._config})" + + @property + def config(self): + return self._config + + @property + def raw_data(self): + return self._data + + def refresh(self): + # self.log.info("refresh") + data = self._config.get_data() + count = 0 + # print(data) + if data is not None: + self.beginResetModel() + self._data.clear() + self._data = data + self.endResetModel() + count = len(data) + # print(data) + # print(self._data) + self.layoutChanged.emit() + self._main_window.update_status(f"{count} Einträge geladen") + + def rowCount(self, parent=QModelIndex()): + # self.log.info("rowCount %s: %d", self, len(self._data)) + # The length of the outer list. + if self._data is None: + return 0 + return len(self._data) + + def headerData(self, col, orientation, role=Qt.ItemDataRole.DisplayRole): + # self.log.info(f"{self._config.header[col]}") + if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: + return self._config.header[col][ColumnEntry.COLUMN_LABEL] + if orientation == Qt.Orientation.Vertical and role == Qt.ItemDataRole.DisplayRole: + return str(col + 1) + + def data(self, index, role=Qt.ItemDataRole.DisplayRole): + if self._data is None: + return None + value = self._data[index.row()][index.column()] + # print('{}:: {}:: {}:: {}: {}'.format(index, role, self._config.header[index.column()][ColumnEntry.COLUMN_TYPE], value, type(value))) + match role: + case Qt.ItemDataRole.DisplayRole: + return get_display_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.EditRole: + return get_edit_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.DecorationRole: + return get_decoration_value(value, self._config.header[index.column()], self._config.main_window) + case Qt.ItemDataRole.BackgroundRole: + return get_background_value(value, self._config.header[index.column()], self._config.main_window) + + def columnCount(self, index=QModelIndex()): + # self.log.info("rowCount %s: %d", self, len(self._config.header)) + return len(self._config.header) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole) -> bool: + # self._config.log.info(f"{index}: {role}") + if role == Qt.ItemDataRole.EditRole: + self._data[index.row()][index.column()] = value + # self._config.log.info(f"{index.row()}-{index.column()}: {self._data[index.row()][index.column()]}") + self.dataChanged.emit(index, index) + return True + if role == Qt.ItemDataRole.CheckStateRole: + print("role == Qt.ItemDataRole.CheckStateRole") + checked = value == Qt.CheckState.Checked + self._data[index.row()][index.column()] = checked + return False + + def flags(self, index): + if self._config.header[index.column()][ColumnEntry.COLUMN_NAME] == 'id': + return Qt.ItemFlag.ItemIsEnabled + return Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsUserTristate diff --git a/kontor-gui/gui/worker.py b/kontor-gui/gui/worker.py new file mode 100644 index 0000000..5f0d4d6 --- /dev/null +++ b/kontor-gui/gui/worker.py @@ -0,0 +1,27 @@ + +from PySide6.QtCore import Signal, QThread + + +class VideoDownloader(QThread): + # Signal for the window to establish the maximum value + # of the progress bar. + setTotalProgress = Signal(int) + # Signal to increase the progress. + setCurrentProgress = Signal(int) + # Signal to be emitted when the file has been downloaded successfully. + succeeded = Signal() + + def __init__(self, kontor_db, log): + super().__init__() + self.kontor_db = kontor_db + self.log = log + + def run(self): + self.log.info("download videos for table MediaFile") + download_entries = self.kontor_db.get_download_list() + self.setTotalProgress.emit(len(download_entries)) + for index, entry in enumerate(download_entries): + self.kontor_db.download_file(entry) + self.setCurrentProgress.emit(index) + self.succeeded.emit() + diff --git a/kontor-gui/main.py b/kontor-gui/main.py new file mode 100644 index 0000000..9ec73f1 --- /dev/null +++ b/kontor-gui/main.py @@ -0,0 +1,48 @@ +""" +PySide6 GUI for Kontor +""" +import sys +import logging.config +from pathlib import Path +from platformdirs import PlatformDirs +from PySide6.QtWidgets import QApplication +import yaml +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from kontor_schema.base import Base + +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from gui.main_window import MainWindow + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +args = parser.parse_args() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + dirs = PlatformDirs(args.config) + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'] + )) + logging_config = Path(dirs.user_config_dir, 'logging-config.yaml') + with open(logging_config, 'rt') as f: + config = yaml.safe_load(f.read()) + logging.config.dictConfig(config) + logger = logging.getLogger('development') + # engine = create_engine(connect_string, echo=True) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + + window = MainWindow(engine, logger) + window.show() + app.exec() diff --git a/kontor-gui/pyproject.toml b/kontor-gui/pyproject.toml new file mode 100644 index 0000000..db3be97 --- /dev/null +++ b/kontor-gui/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "kontor-gui" +version = "0.1.0" +description = "Kontor GUI" +authors = [ + {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"}, +] +dependencies = [ + "platformdirs", + "pyyaml", + "PySide6", +] +requires-python = ">=3.11" +readme = "README.md" +license = {text = "MIT"} + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + + +[tool.pdm] +distribution = true + +[dependency-groups] +dev = ["-e file:///${PROJECT_ROOT}/../kontor-schema#egg=kontor-schema"] diff --git a/kontor-gui/pysidedeploy.spec b/kontor-gui/pysidedeploy.spec new file mode 100644 index 0000000..6fb865d --- /dev/null +++ b/kontor-gui/pysidedeploy.spec @@ -0,0 +1,98 @@ +[app] + +# title of your application +title = kontor + +# project directory. the general assumption is that project_dir is the parent directory +# of input_file +project_dir = /home/tpeetz/projects/kontor/python/kontor-gui + +# source file path +input_file = /home/tpeetz/projects/kontor/python/kontor-gui/main.py + +# directory where exec is stored +exec_directory = . + +# path to .pyproject project file +project_file = + +# application icon +icon = /usr/local/lib/python3.11/dist-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg + +[python] + +# python path +python_path = /home/tpeetz/projects/kontor/python/kontor-gui/env/bin/python + +# python packages to install +packages = Nuitka==2.4.8 + +# buildozer = for deploying Android application +android_packages = buildozer==1.5.0,cython==0.29.33 + +[qt] + +# comma separated path to qml files required +# normally all the qml files required by the project are added automatically +qml_files = + +# excluded qml plugin binaries +excluded_qml_plugins = + +# qt modules used. comma separated +modules = Widgets,DBus,Core,Gui + +# qt plugins used by the application +plugins = platformthemes,imageformats,platforms,generic,iconengines,egldeviceintegrations,styles,xcbglintegrations,platforms/darwin,platforminputcontexts,accessiblebridge + +[android] + +# path to pyside wheel +wheel_pyside = + +# path to shiboken wheel +wheel_shiboken = + +# plugins to be copied to libs folder of the packaged application. comma separated +plugins = + +[nuitka] + +# usage description for permissions requested by the app as found in the info.plist file +# of the app bundle +# eg = extra_args = --show-modules --follow-stdlib +macos.permissions = + +# mode of using nuitka. accepts standalone or onefile. default is onefile. +mode = onefile + +# (str) specify any extra nuitka arguments +extra_args = --quiet --noinclude-qt-translations + +[buildozer] + +# build mode +# possible options = [release, debug] +# release creates an aab, while debug creates an apk +mode = debug + +# contrains path to pyside6 and shiboken6 recipe dir +recipe_dir = + +# path to extra qt android jars to be loaded by the application +jars_dir = + +# if empty uses default ndk path downloaded by buildozer +ndk_path = + +# if empty uses default sdk path downloaded by buildozer +sdk_path = + +# other libraries to be loaded. comma separated. +# loaded at app startup +local_libs = + +# architecture of deployed platform +# possible values = ["aarch64", "armv7a", "i686", "x86_64"] +arch = + diff --git a/kontor-gui/pyvenv.cfg b/kontor-gui/pyvenv.cfg new file mode 100644 index 0000000..e6324f5 --- /dev/null +++ b/kontor-gui/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.11.2 +executable = /usr/bin/python3.11 +command = /usr/bin/python -m venv /home/tpeetz/projects/kontor/python/kontor-gui diff --git a/kontor-gui/requirements.txt b/kontor-gui/requirements.txt new file mode 100644 index 0000000..a6e9a93 --- /dev/null +++ b/kontor-gui/requirements.txt @@ -0,0 +1,6 @@ +-e ../kontor-schema + +platformdirs +pyyaml +PySide6 + diff --git a/kontor-gui/res/application-export.png b/kontor-gui/res/application-export.png new file mode 100644 index 0000000000000000000000000000000000000000..555887a28d64bc812c4dfa98a6ff1da1927b7792 GIT binary patch literal 513 zcmV+c0{;DpP)A0|fCeqz@KJVX8nnt2D9k2UrOajFq_SwR4OUbbUJ;HgeDQF66oE777mBiWdd2F(NKOO zk$Ax17hTui{#6jPXfz7dY87I!*bgRr4TVA=afmd=7~nkYBq$b(su#^>Q?2y;%rJ~= zA;j4sgM^B|5YOb(M4cc`5q!^h_wPg5is0Dq{42l!6{r`}{QE*r00000NkvXXu0mjf Djqud07&@TR)18#v5Hqj8@|Bnpn>z;V`CueWd< z7oI05i2^A#D9Qz9v#hs`;>*D7{eB7>0ppH2SLstJO-8BwR~M`A7kF ztyTlI66tgrtyT-MSPa~yU>3AuKR54&OXJih@;oco-=1sDLQa}`@LC7Iy> O0000dz zNLw#yfl;S$OS+NxP!T0`q27a_KcR=-dJ5{Xm+WEf!J>k|BqE5#C}ImGwQ$$<(wdh& z>AIJ5zVFx9lo8J{@aWY%oIP6CAZKYHol)uRf|554m_YTnpAlGl zL9{5M4)qVt#u>|mLQVrh46q|<)MP|~t8EE-8crSv>~io{4+abW2A~lV0z>OM)mbBD zmWtqV;+?9YGlhpQQ@M9zi{*KRG_T5PQ_nvC9tMo_mT6XEIU5;J+S?d(HD_0f!1ES7 z7@Uj?4!kD>&?r8e&c1jan^9Cs!}3z0d7Izafj)-}rbYZR}EdihS6`M#Fcy= zaw{u#mV;BNWe3{Sqiq;{_M>cOP*8MhP1CLQf3t|C2tpM55ycYh7RL%}Yf+Bp#e!~C z2(WG^ZnlxC>igv!sW2GM8SslVJ(oFwkp~5*6E^0rT$Q zya}^2lB>ITrX0zGEa!lc90$HC4~#^g#%ZB2j3vzHhi;TO$n{?Wwx(^0>^QA_x~H^z zTf=2SxY0rCf|fl@rb7TXYr%pE(0=u@G literal 0 HcmV?d00001 diff --git a/kontor-gui/res/cross.png b/kontor-gui/res/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9fa6dd36ee8165272a13dd263f573507c78ca6 GIT binary patch literal 544 zcmV+*0^j|KP)L-ku(! z6_?D-?!0+#=VtbVZQGdTnZt~apI_HPK#=zVadHM((E`e*C+RoFb)Qo8-U{Lb7{`Tz zw4B8FZ|o?Wox+p=1wp47C;7ar*Xu~;a?<=sjPv>+otDjJ6FaGt!YnNyxQQhp)G1V! zk;r6ZtyV)c8pTbiRAnHRNXTxti%=+p$4aG2*+mMM&xrdiU^`VPk^N*+HX98^7!HT% zwA%;A{T)GxeQ|RcxyzV%! z$AbYD+)i1R64zB?q}NmTz}5|04~J#YG_goA*Eq(QJvp7pG14hUj1pZ^t>3S*xqHUO ze~r;}zTY_XkZ*}d@gm!;N90h8nBQenBUZ_ukZKNicn*hc_Pmc!Jn|2=s<~}>!Sb>Qm3!QP46b_JFw5YU70Tu$X}=T}ez@@t%j iG9vD)nDux55?}x$+|UyQVK_bj0000tYd4K$mX5uyr2F@fdffp{&DSHtl4|Bn9=ukpCx`#%PTUr-D(@b7;C zhJXJjrnw{;1KBM=6&|E`fsNrGL!Y6%zUh}QUl`(@V)PmQFtotEKmafTHP0uP}7&%m4pcKQ#X)BiAJ2y+PrtBI>9eEIt2-_c7)?*Lsf z5vX5*Af@m-wBJRV%#Fiz=BdPr0!2^a2d-yv7gSU);Z6{`~Oy?E5qSnHUln4*Y%!){F!Y1~5WXoC44g zb7l_)X~q^V6MmWR7e3wj|L|Wb!|^}Y86N$^h+kv_Kw-fP!~#If$6(B4$)LlK#&G6; zC&ShMSAb$5tA9Xg5dOsg@-z^@3;;QS9f&!gG&3|;{Da~@Q2ZB({s%XJ5&#fj0J|In U+D>(nEdT%j07*qoM6N<$g0@&8X#fBK literal 0 HcmV?d00001 diff --git a/kontor-gui/src/kontor_gui/__init__.py b/kontor-gui/src/kontor_gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-gui/tests/__init__.py b/kontor-gui/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-schema/.gitignore b/kontor-schema/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/kontor-schema/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/kontor-schema/.python-version b/kontor-schema/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/kontor-schema/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/kontor-schema/README.md b/kontor-schema/README.md new file mode 100644 index 0000000..e69de29 diff --git a/kontor-schema/pyproject.toml b/kontor-schema/pyproject.toml new file mode 100644 index 0000000..2e572f9 --- /dev/null +++ b/kontor-schema/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "kontor-schema" +version = "0.1.0" +description = "Kontor Schema Library" +readme = "README.md" +authors = [ + { name = "Thomas Peetz", email = "thomas.peetz@ingenieurbuero-peetz.de" } +] +requires-python = ">=3.13" +dependencies = [ + "beautifulsoup4>=4.13.4", + "requests>=2.32.3", + "sqlalchemy>=2.0.40", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/kontor-schema/src/kontor_schema/__init__.py b/kontor-schema/src/kontor_schema/__init__.py new file mode 100644 index 0000000..2f0718b --- /dev/null +++ b/kontor-schema/src/kontor_schema/__init__.py @@ -0,0 +1,10 @@ +from enum import Enum, auto + +from .admin import User, Token, Role, AuthorizationMatrix, ModuleData, MailAccount, Mail +from .bookshelf import Article, Book, Author, BookshelfPublisher, ArticleAuthor, BookAuthor +from .comic import Comic, Artist, Publisher, Issue, StoryArc, TradePaperback, Volume, ComicWork, WorkType +from .metadata import MetaDataTable, MetaDataColumn +from .tysc import Card, CardSet, Sport, Team, FieldPosition, Rooster, Player, Vendor +from .media import MediaFile, MediaArticle, MediaVideo +from .base import Base +from .database import KontorDB, ColumnEntry diff --git a/kontor-schema/src/kontor_schema/admin.py b/kontor-schema/src/kontor_schema/admin.py new file mode 100644 index 0000000..bbc5c14 --- /dev/null +++ b/kontor-schema/src/kontor_schema/admin.py @@ -0,0 +1,82 @@ +from datetime import datetime + +<<<<<<<< HEAD:kontor-schema/src/kontor_schema/admin.py +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +======== +from sqlalchemy import Boolean, Column, ForeignKey, Integer, String +>>>>>>>> 934ef82 (Resolve "evaluate uv"):kontor-api/src/schema/admin.py +from sqlalchemy.orm import relationship, mapped_column, Mapped + +from .base import Base, BaseMixin + + +class User(Base, BaseMixin): + __tablename__ = 'user' + first_name = Column(String(255)) + last_name = Column(String(255)) + user_name = Column(String(255), nullable=False) + email = Column(String(255)) + password = Column(String(255)) + enabled = Column(BIT(1)) + matrix = relationship("AuthorizationMatrix") + tokens = relationship("Token") + + def get_full_name(self) -> str: + full_name = "" + if self.first_name is not None: + full_name += self.first_name + if self.last_name is not None: + if len(full_name) > 0: + full_name += " " + full_name += self.last_name + return full_name + + +class Token(Base, BaseMixin): + __tablename__ = "token" + token = Column(String(255), nullable=False, unique=True) + name = Column(String(255)) + last_used_date: Mapped[datetime] = mapped_column() + enabled = Column(BIT(1)) + user_id = Column(String(255), ForeignKey("user.id"), nullable=False) + user = relationship("User", back_populates="tokens") + + +class Role(Base, BaseMixin): + __tablename__ = "role" + name = Column(String(255), nullable=False) + matrix = relationship("AuthorizationMatrix") + + +class AuthorizationMatrix(Base, BaseMixin): + __tablename__ = "authorization_matrix" + user_id = Column(String, ForeignKey("user.id"), nullable=False) + user = relationship("User", back_populates="matrix") + role_id = Column(String, ForeignKey("role.id"), nullable=False) + role = relationship("Role", back_populates="matrix") + + +class ModuleData(Base, BaseMixin): + __tablename__ = "module_data" + module_name = Column(String(255), nullable=False) + import_data = Column(BIT(1)) + + +class MailAccount(Base, BaseMixin): + __tablename__ = "mail_account" + host = Column(String(255)) + port = Column(Integer) + protocol = Column(String(255)) + user_name = Column(String(255)) + password = Column(String(255)) + start_tls = Column(BIT(1)) + + +class Mail(Base, BaseMixin): + __tablename__ = "mail" + folder: Mapped[str] = mapped_column() + subject: Mapped[str] = mapped_column() + body: Mapped[str] = mapped_column() + sent_date: Mapped[datetime] = mapped_column() + received_date: Mapped[datetime] = mapped_column() diff --git a/kontor-schema/src/kontor_schema/base.py b/kontor-schema/src/kontor_schema/base.py new file mode 100644 index 0000000..4a354e7 --- /dev/null +++ b/kontor-schema/src/kontor_schema/base.py @@ -0,0 +1,31 @@ +import uuid +from datetime import datetime + +from sqlalchemy import func, Column, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + pass + + +class BaseMixin: + id = Column(String(255), primary_key=True, default=uuid.uuid4()) + # id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) + # created_date = Column(DateTime) + created_date: Mapped[datetime] = mapped_column(default=func.now()) + # last_modified_date = Column(DateTime) + last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) + # version = Column(Integer) + version: Mapped[int] = mapped_column(default=0) + + +class BaseVideoMixin: + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) diff --git a/kontor-schema/src/kontor_schema/bookshelf.py b/kontor-schema/src/kontor_schema/bookshelf.py new file mode 100644 index 0000000..91e0ae4 --- /dev/null +++ b/kontor-schema/src/kontor_schema/bookshelf.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Article(Base, BaseMixin): + __tablename__ = 'article' + title = Column(String(length=255), unique=True) + article_authors = relationship("ArticleAuthor") + + +class Author(Base, BaseMixin): + __tablename__ = 'author' + first_name = Column(String(255)) + last_name = Column(String(255)) + article_authors = relationship("ArticleAuthor") + book_authors = relationship("BookAuthor") + + +class BookshelfPublisher(Base, BaseMixin): + __tablename__ = 'bookshelf_publisher' + name = Column(String(length=255), unique=True) + books = relationship("Book") + + +class Book(Base, BaseMixin): + __tablename__ = 'book' + isbn = Column(String(255), unique=True) + title = Column(String(255)) + year = Column(Integer, nullable=False) + publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) + publisher = relationship('BookshelfPublisher', back_populates="books") + book_authors = relationship("BookAuthor") + + +class ArticleAuthor(Base, BaseMixin): + __tablename__ = 'article_author' + article_id = Column(String, ForeignKey('article.id'), nullable=False) + article = relationship('Article', back_populates="article_authors") + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="article_authors") + + +class BookAuthor(Base, BaseMixin): + __tablename__ = 'book_author' + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="book_authors") + book_id = Column(String, ForeignKey('book.id'), nullable=False) + book = relationship('Book', back_populates="book_authors") diff --git a/kontor-schema/src/kontor_schema/comic.py b/kontor-schema/src/kontor_schema/comic.py new file mode 100644 index 0000000..1052d79 --- /dev/null +++ b/kontor-schema/src/kontor_schema/comic.py @@ -0,0 +1,100 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Publisher(Base, BaseMixin): + __tablename__ = "publisher" + name = Column(String(length=255), unique=True) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base, BaseMixin): + __tablename__ = 'comic' + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' + + +class Volume(Base, BaseMixin): + __tablename__ = "volume" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base, BaseMixin): + __tablename__ = "trade_paperback" + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base, BaseMixin): + __tablename__ = "story_arc" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base, BaseMixin): + __tablename__ = "issue" + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base, BaseMixin): + __tablename__ = "artist" + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class WorkType(Base, BaseMixin): + __tablename__ = "worktype" + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})' + + def __str__(self): + return f'{self.name}({self.id})' + + +class ComicWork(Base, BaseMixin): + __tablename__ = "comic_work" + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/kontor-schema/src/kontor_schema/database.py b/kontor-schema/src/kontor_schema/database.py new file mode 100644 index 0000000..54d3596 --- /dev/null +++ b/kontor-schema/src/kontor_schema/database.py @@ -0,0 +1,396 @@ +import json +import uuid +from datetime import datetime +from enum import Enum, auto +from logging import Logger +from pathlib import Path + +from sqlalchemy import Engine, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport +from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType +from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author +from .admin import Mail, MailAccount, ModuleData, Role, User, Token, AuthorizationMatrix +from .metadata import MetaDataTable, MetaDataColumn +from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile + + +class ColumnEntry(Enum): + COLUMN_NAME = 'column' + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = 'type' + COLUMN_WIDGET = 'widget' + + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + + +class ExportType(Enum): + JSON = "JSON" + YAML = "YAML" + SQLITE = "SQLite" + + +class KontorDB: + + def __init__(self, db_engine: Engine, log: Logger): + self.engine = db_engine + self.registry = {} + self.init_registry() + self.log = log + + def init_registry(self): + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaActor.__tablename__] = MediaActor + self.registry[MediaActorFile.__tablename__] = MediaActorFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[AuthorizationMatrix.__tablename__] = AuthorizationMatrix + self.registry[Token.__tablename__] = Token + self.registry[User.__tablename__] = User + self.registry[Role.__tablename__] = Role + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail + + def get_table_names(self) -> list: + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = session.scalars(select(MetaDataTable)).all() + result = [table.table_name for table in tables] + return result + + def get_table_by_name(self, table_name: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + _filter = {'table_name': table_name} + with __session__() as session: + table = session.query(MetaDataTable).filter_by(**_filter).one() + result['id'] = table.id + result['table_name'] = table.table_name + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + __session__ = sessionmaker(self.engine) + columns = list() + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + if view_only: + _filters['is_shown'] = True + with __session__() as session: + columns = session.query(MetaDataColumn).filter_by(**_filters).all() + for column in columns: + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + return meta_data + + def get_columns(self, table_name: str) -> dict: + columns = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } + return columns + + def get_filters(self, table_name: str) -> dict: + _filter_map = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id'], 'show_filter': True} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } + return _filter_map + + def data(self, table_name: str, columns: dict, filters: dict) -> list: + data = [] + __session__ = sessionmaker(self.engine) + table = self.registry[table_name] + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.scalars(select(table)).all() + else: + entries = session.scalars(select(table).filter_by(**filters)).all() + for entry in entries: + # self.log.info("data: %s", entry) + row = [] + for order in columns.keys(): + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + ref = getattr(entry, ref_table) + value = getattr(ref, ref_column) + row.append(value) + else: + row.append(getattr(entry, column_name)) + data.append(row) + # self.log.info("data: %s", data) + return data + + def export_db(self, export_type: ExportType, export_file_name: str) -> dict: + results = {} + db = {} + export_table_list = self.get_table_names() + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + self.log.info(f"table {table} is not registered") + continue + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order][ColumnEntry.COLUMN_NAME] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError: + pass + entries.append(entry) + db[table] = entries + results[table] = len(entries) + match export_type: + case ExportType.JSON: + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case ExportType.YAML: + pass + case ExportType.SQLITE: + pass + self.log.info(f"{len(results)} tables exported") + return results + + def import_db(self, import_file_name: str) -> dict: + result = {} + import_file = Path(import_file_name) + if not import_file.exists(): + self.log.info(f"File {import_file_name} does not exist. Do nothing.") + return result + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + self.log.info(f"{table}: {len(json_load[table])}") + result[table] = self.import_table(table, json_load[table]) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + return result + + def import_table(self, table_name: str, items:list) -> dict: + result = {} + updated = [] + added = [] + remaining = [] + existing_ids = self.get_ids(table_name) + self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") + for item in items: + current_id = item['id'] + # print(f"import item: {item}") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + self.log.info(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + self.log.info(f"Could not add item, due to: {error.detail}") + if len(existing_ids) > 0: + print(f"remaining items: {existing_ids}") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def add_entry(self, table_name: str, update_item: dict): + self.log.debug(f"add entry to table {table_name} with {update_item}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() + + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + # self.log.info("update entry to table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + self.log.info(f"{key} has changed: {existing_value} != {update_value}") + setattr(existing_item, key, update_value) + session.commit() + changed = True + self.log.info(f"update {key} with {update_value}") + return changed + + def add_link(self, link: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, 'download': media_file.should_download} + except IntegrityError as error: + session.rollback() + result['error'] = error.orig + return result + + def update_titles(self) -> dict: + update_list = {} + __session__ = sessionmaker(self.engine) + _filter = { 'review': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + for link in links: + url = link.url + if url is None: + continue + link.update_title() + session.commit() + update_list[link.id] = link.title + return update_list + + def get_download_list(self) -> list: + download_list = [] + __session__ = sessionmaker(self.engine) + _filter = { 'should_download': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + for link in links: + url = link.url + if url is None: + continue + download_list.append(link.id) + return download_list + + def download_file(self, entry_id: str, download_dir = "/data/media", dl_tool = "yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + # self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() + + def check_files(self): + pass diff --git a/kontor-schema/src/kontor_schema/media.py b/kontor-schema/src/kontor_schema/media.py new file mode 100644 index 0000000..a22be99 --- /dev/null +++ b/kontor-schema/src/kontor_schema/media.py @@ -0,0 +1,93 @@ +import re +import subprocess +from datetime import datetime +from pathlib import Path + +import requests +from bs4 import BeautifulSoup +from sqlalchemy import Column, String, ForeignKey +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin, BaseVideoMixin + + +class MediaFile(Base, BaseMixin, BaseVideoMixin): + __tablename__ = 'media_file' + media_actor_files = relationship("MediaActorFile") + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' + + def update_title(self) -> None: + print(f"update title for {self.url}") + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + self.title = title + self.review = 0 + except: + self.title = None + self.review = 1 + self.last_modified_date = datetime.now() + + def download_file(self, download_dir: str, dl_tool: str): + print(f"download file for {self.url} to {download_dir}") + result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + file_name = self.__parse_output__(lines_list) + if file_name is None: + self.review = True + self.should_download = True + self.file_name = None + else: + download_file = Path(file_name) + self.should_download = False + self.file_name = download_file.name + self.cloud_link = str(download_file.absolute()) + self.last_modified_date = datetime.now() + + def __parse_output__(self, lines_list): + self.file_name = None + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + self.file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + self.file_name = line[-file_len:] + return self.file_name + + +class MediaActor(Base, BaseMixin): + __tablename__ = 'media_actor' + name = Column(String(255)) + media_actor_files = relationship("MediaActorFile") + + +class MediaActorFile(Base, BaseMixin): + __tablename__ = 'media_actor_file' + media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False) + media_actor = relationship("MediaActor", back_populates="media_actor_files") + media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=False) + media_file = relationship("MediaFile", back_populates="media_actor_files") + + +class MediaArticle(Base, BaseMixin): + __tablename__ = 'media_article' + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + + +class MediaVideo(Base, BaseMixin, BaseVideoMixin): + __tablename__ = 'media_video' diff --git a/kontor-schema/src/kontor_schema/metadata.py b/kontor-schema/src/kontor_schema/metadata.py new file mode 100644 index 0000000..950cebe --- /dev/null +++ b/kontor-schema/src/kontor_schema/metadata.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, String, ForeignKey, Integer +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class MetaDataTable(Base, BaseMixin): + __tablename__ = 'meta_data_table' + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + return f'MetaDataTable({self.id} {self.table_name})' + + def __str__(self): + return f'{self.table_name}({self.id})' + + +class MetaDataColumn(Base, BaseMixin): + __tablename__ = 'meta_data_column' + column_name = Column(String(255), nullable=False) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + column_modifier = Column(String(255), nullable=True) + column_order = Column(Integer) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) + + def __repr__(self): + if self.column_name is None: + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' + else: + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' + + def __str__(self): + return f'{self.column_name}({self.id})' diff --git a/kontor-schema/src/kontor_schema/tysc.py b/kontor-schema/src/kontor_schema/tysc.py new file mode 100644 index 0000000..32c88f1 --- /dev/null +++ b/kontor-schema/src/kontor_schema/tysc.py @@ -0,0 +1,100 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Sport(Base, BaseMixin): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base, BaseMixin): + __tablename__ = "team" + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) + sport = relationship("Sport", back_populates="teams") + roosters = relationship("Rooster") + + +class FieldPosition(Base, BaseMixin): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base, BaseMixin): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) + roosters = relationship("Rooster") + + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + + +class Rooster(Base, BaseMixin): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) + position = relationship("FieldPosition", back_populates="roosters") + cards = relationship("Card") + + +class Vendor(Base, BaseMixin): + __tablename__ = "vendor" + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base, BaseMixin): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base, BaseMixin): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("CardSet", back_populates="cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("Rooster", back_populates="cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/kontor-schema/uv.lock b/kontor-schema/uv.lock new file mode 100644 index 0000000..5323172 --- /dev/null +++ b/kontor-schema/uv.lock @@ -0,0 +1,161 @@ +version = 1 +revision = 1 +requires-python = ">=3.13" + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[package]] +name = "greenlet" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/9c/666d8c71b18d0189cf801c0e0b31c4bfc609ac823883286045b1f3ae8994/greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", size = 183685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/43/c0b655d4d7eae19282b028bcec449e5c80626ad0d8d0ca3703f9b1c29258/greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", size = 269131 }, + { url = "https://files.pythonhosted.org/packages/7c/7d/c8f51c373c7f7ac0f73d04a6fd77ab34f6f643cb41a0d186d05ba96708e7/greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", size = 637323 }, + { url = "https://files.pythonhosted.org/packages/89/65/c3ee41b2e56586737d6e124b250583695628ffa6b324855b3a1267a8d1d9/greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", size = 651430 }, + { url = "https://files.pythonhosted.org/packages/f0/07/33bd7a3dcde1db7259371d026ce76be1eb653d2d892334fc79a500b3c5ee/greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", size = 645798 }, + { url = "https://files.pythonhosted.org/packages/35/5b/33c221a6a867030b0b770513a1b78f6c30e04294131dafdc8da78906bbe6/greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", size = 648271 }, + { url = "https://files.pythonhosted.org/packages/4d/dd/d6452248fa6093504e3b7525dc2bdc4e55a4296ec6ee74ba241a51d852e2/greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", size = 606779 }, + { url = "https://files.pythonhosted.org/packages/9d/24/160f04d2589bcb15b8661dcd1763437b22e01643626899a4139bf98f02af/greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", size = 1117968 }, + { url = "https://files.pythonhosted.org/packages/6c/ff/c6e3f3a5168fef5209cfd9498b2b5dd77a0bf29dfc686a03dcc614cf4432/greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", size = 1145510 }, + { url = "https://files.pythonhosted.org/packages/dc/62/5215e374819052e542b5bde06bd7d4a171454b6938c96a2384f21cb94279/greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", size = 296004 }, + { url = "https://files.pythonhosted.org/packages/62/6d/dc9c909cba5cbf4b0833fce69912927a8ca74791c23c47b9fd4f28092108/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b", size = 629900 }, + { url = "https://files.pythonhosted.org/packages/5e/a9/f3f304fbbbd604858ff3df303d7fa1d8f7f9e45a6ef74481aaf03aaac021/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", size = 635270 }, + { url = "https://files.pythonhosted.org/packages/34/92/4b7b4e2e23ecc723cceef9fe3898e78c8e14e106cc7ba2f276a66161da3e/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", size = 632534 }, + { url = "https://files.pythonhosted.org/packages/da/7f/91f0ecbe72c9d789fb7f400b39da9d1e87fcc2cf8746a9636479ba79ab01/greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/9f/59/e449a44ce52b13751f55376d85adc155dd311608f6d2aa5b6bd2c8d15486/greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", size = 593697 }, + { url = "https://files.pythonhosted.org/packages/bb/09/cca3392927c5c990b7a8ede64ccd0712808438d6490d63ce6b8704d6df5f/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", size = 1105762 }, + { url = "https://files.pythonhosted.org/packages/4d/b9/3d201f819afc3b7a8cd7ebe645f1a17799603e2d62c968154518f79f4881/greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", size = 1125173 }, + { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "kontor-schema" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "requests" }, + { name = "sqlalchemy" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887 }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367 }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806 }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131 }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364 }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482 }, + { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704 }, + { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564 }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, +] diff --git a/kontor-scripts/.python-version b/kontor-scripts/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/kontor-scripts/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/kontor-scripts/Makefile b/kontor-scripts/Makefile new file mode 100644 index 0000000..e66d08d --- /dev/null +++ b/kontor-scripts/Makefile @@ -0,0 +1,15 @@ + +clean: + find . -name '*.py[co]' -delete + +test: + python -m pytest \ + -v \ + --cov=kontor \ + --cov-report=term \ + --cov-report=html:coverage-report \ + tests/ + +docker: clean + docker build -t kontor:latest . + diff --git a/kontor-scripts/README.md b/kontor-scripts/README.md new file mode 100644 index 0000000..3bb42eb --- /dev/null +++ b/kontor-scripts/README.md @@ -0,0 +1,3 @@ +# kontor-scripts + + diff --git a/kontor-scripts/check_kontor.py b/kontor-scripts/check_kontor.py new file mode 100644 index 0000000..e75a543 --- /dev/null +++ b/kontor-scripts/check_kontor.py @@ -0,0 +1,183 @@ +""" +Checks the database kontor +""" +from enum import Enum, auto +import json +import mariadb +import requests +from pathlib import Path +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + +from config import get_logger, get_database_cursors + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor') +parser.add_argument('--file', '-f') +parser.add_argument('--dir', '-d') +parser.add_argument('--dry-run', '-m', action='store_true') +parser.add_argument('--reset-cloud-link', '-r', action='store_true') +args = parser.parse_args() + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + +class FileStatus: + id: str | None = None + status_type: StatusType = StatusType.UNKNOWN + + def get_response(self, response: dict): + self.status_type = StatusType.FILE_NAME + self.id = response['id'] + + +def get_status_of_file(found_file: Path, cursor, log) -> FileStatus: + status = FileStatus() + try: + cursor.execute(f'SELECT id, cloud_link FROM media_file WHERE file_name="{found_file.name}"') + rows = cursor.fetchall() + if len(rows) == 1: + status.status_type = StatusType.FILE_NAME + status.id = rows[0][0] + except mariadb.Error as error: + log.debug(f'select failed with {error}') + try: + cursor.execute(f'SELECT id FROM media_file WHERE id="{found_file.stem}"') + rows = cursor.fetchall() + if len(rows) == 1: + status.status_type = StatusType.FILE_ID + status.id = rows[0][0] + if len(rows) > 1: + status.status_type = StatusType.DUPLICATE + for row in rows: + log.info(f"found {row[0]} with {found_file}") + except mariadb.Error as error: + log.debug(f'select failed with {error}') + try: + cursor.execute(f'SELECT id FROM media_file WHERE cloud_link LIKE "%{found_file.stem}%"') + rows = cursor.fetchall() + if len(rows) == 1: + status.id = rows[0][0] + if rows[0][0] == found_file.stem: + status.status_type = StatusType.CLOUD_LINK_ID + else: + status.status_type = StatusType.CLOUD_LINK + except mariadb.Error as error: + log.debug(f'select failed with {error}') + response = requests.get(f"http://127.0.0.1:8800/media/files/{found_file.stem}") + log.debug(f"Status: {response.status_code}") + if response.status_code == 200: + status.status_type = StatusType.FILE_ID + status.id = response.json()['id'] + return status + +def rename_files_to_id(media_dir, dry_run, conn, log): + media_path = Path(media_dir) + cursor = conn.cursor() + for file in media_path.iterdir(): + log.debug('found file: {}'.format(file.name)) + status: FileStatus = get_status_of_file(file, cursor, log) + file_id = status.id + if not file_id: + log.info(f"ID of file {file.name} is unknown") + continue + new_file_path = file.with_name(f"{file_id}{file.suffix}") + match status.status_type: + case StatusType.FILE_NAME: + log.info(f'status of {file.name} is file_name') + rename_file(file, new_file_path, dry_run, log) + update_cloud_link(file_id, new_file_path, conn, dry_run, log) + case StatusType.FILE_ID: + log.info(f'status of {file.name} is file_id') + update_cloud_link(file_id, new_file_path, conn, dry_run, log) + case StatusType.CLOUD_LINK: + log.info(f'status of {file.name} is cloud_link') + rename_file(file, new_file_path, dry_run, log) + update_cloud_link(file_id, new_file_path, conn, dry_run, log) + case StatusType.CLOUD_LINK_ID: + log.debug(f'status of {file.name} is cloud_link_id') + update_cloud_link(file_id, new_file_path, conn, dry_run, log) + case StatusType.DUPLICATE: + log.info(f'status of {file.name} is duplicate') + case StatusType.UNKNOWN: + log.info(f'status of {file.name} is unknown') + +def rename_file(current_file, new_file_path, dry_run, log): + if dry_run: + log.info('rename file {} to {}'.format(current_file.name, new_file_path.name)) + else: + current_file.rename(Path(new_file_path)) + +def update_cloud_link(file_id, file_path, conn, dry_run, log): + cursor = conn.cursor() + log.debug(f'update entry {file_id} with {file_path.absolute()}') + if dry_run: + log.debug(f'UPDATE media_file: cloud_link={file_path.absolute()}') + else: + cursor.execute('UPDATE media_file SET cloud_link="{}" WHERE id="{}"'.format(file_path.absolute(), file_id)) + conn.commit() + +def reset_cloud_link(conn, dry_run, log): + cursor = conn.cursor() + if dry_run: + log.info('UPDATE media_file SET cloud_link=""') + else: + cursor.execute('UPDATE media_file SET cloud_link="" WHERE id is NOT NULL') + conn.commit() + +def check_file_with_db(data_file: Path, m_conn, log): + log.info(f"read json file: {data_file}") + cursor = m_conn.cursor() + with open(data_file, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + log.info(f"{table}: {len(json_load[table])}") + items = json_load[table] + for item in items: + item_id = item['id'] + select_statement = f"SELECT * FROM {table} WHERE id='{item_id}'" + cursor.execute(select_statement) + rows = cursor.fetchall() + count = len(rows) + log.info(f"{count} entries found for {item_id}") + if count == 0: + log.info(f"entry for {item_id} not found") + if count == 1: + log.info(f"check entry {item_id}") + #log.info(f"entry {rows[0]}") + columns = [] + values = [] + for (key, value) in item.items(): + columns.append(key) + values.append(value) + for index, _ in enumerate(columns): + log.info(f"compare {values[index]} with {rows[0][index]}") + + + +if __name__ == '__main__': + log = get_logger(args.verbose, args.config) + log.info("kontor.check_kontor started") + _, m_conn = get_database_cursors(log, args.config) + if args.dir: + log.info("kontor.check_kontor.rename_files_to_id") + rename_files_to_id(args.dir, args.dry_run, m_conn, log) + if args.file: + data_file = Path(args.file) + if data_file.exists(): + log.info("kontor.check_kontor.check_file_with_db") + check_file_with_db(data_file, m_conn, log) + #logger.info("kontor.check_kontor.update_cloud_link_with_found_files") + #update_cloud_link_with_found_files(data_dir, mariadb_conn, args.dry_run) + #logger.info("kontor.check_kontor.get_ids_from_column_cloud_link") + #get_ids_from_column_cloud_link(link_list, mariadb_cursor) + #logger.info('found {} ids in column cloud_link'.format(len(link_list))) + #logger.info("kontor.check_kontor.checking_ids_from_cloud_link") + #checking_ids_from_cloud_link(link_list, mariadb_cursor) + log.info("kontor.check_kontor finished") + diff --git a/kontor-scripts/config.py b/kontor-scripts/config.py new file mode 100644 index 0000000..bf65cd5 --- /dev/null +++ b/kontor-scripts/config.py @@ -0,0 +1,118 @@ +""" +Setup database connections +""" +import sqlite3 +import mariadb +import logging.config +from platformdirs import PlatformDirs +from pathlib import Path +import yaml + + +def get_database_cursors(log, config: str): + dirs = PlatformDirs(config) + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + sqlite_db = db_config["sqlite"]["file"] + log.info('using SQLite3 database {}'.format(sqlite_db)) + sqlite_conn = sqlite3.connect(sqlite_db, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) + mariadb_conn = mariadb.connect( + host=db_config['mariadb']['host'], + port=db_config['mariadb']['port'], + user=db_config['mariadb']['user'], + password=db_config['mariadb']['password'], + database=db_config['mariadb']['database'] + ) + return sqlite_conn, mariadb_conn + + +def create_tables(sqlite_conn, logger, recreate_db, scripts): + logger.info('create_tables') + for table_id in scripts: + create_statement = scripts[table_id]['create'] + drop_statement = scripts[table_id]['drop'] + logger.debug(create_statement) + cursor = sqlite_conn.cursor() + if recreate_db: + logger.debug(drop_statement) + cursor.execute(drop_statement) + cursor.execute(create_statement) + + +def get_logger(level, config: str): + dirs = PlatformDirs(config) + logging_config = Path(dirs.user_config_dir, 'logging-config.yaml') + with open(logging_config, 'rt') as f: + log_config = yaml.safe_load(f.read()) + logging.config.dictConfig(log_config) + logger = logging.getLogger('development') + if level is not None: + match level: + case 0: + logger.setLevel(logging.CRITICAL) + case 1: + logger.setLevel(logging.INFO) + case 2: + logger.setLevel(logging.DEBUG) + case _: + logger.setLevel(logging.INFO) + return logger + + +def get_meta_data(mariadb_conn): + mariadb_cursor = mariadb_conn.cursor() + select_statement = "SELECT id, table_name FROM meta_data_table" + mariadb_cursor.execute(select_statement) + rows = mariadb_cursor.fetchall() + meta_data = {} + for (identifier, table_name) in rows: + table_data = {"name": table_name} + mariadb_cursor.execute("SELECT column_name, column_sync_name, column_type, column_modifier, column_order FROM meta_data_column WHERE table_id=?", (identifier, )) + column_rows = mariadb_cursor.fetchall() + column_list = [] + for (column_name, column_sync_name, column_type, column_modifier, column_order) in column_rows: + column_data = {"column_name": column_name, "column_sync_name": column_sync_name, "column_type": column_type, + "column_modifier": column_modifier, "column_order": column_order} + column_list.append(column_data) + # logger.info(column_list) + table_data["columns"] = column_list + meta_data[identifier] = table_data + return meta_data + + +def get_scripts(meta_data, logger): + scripts_map = {} + for table_id in meta_data: + table_scripts = {} + m_columns = [] + s_columns = [] + columns = [] + for column_data in meta_data[table_id]["columns"]: + column_line = "{} {}".format(column_data["column_sync_name"], column_data["column_type"]) + if column_data["column_modifier"]: + column_line += " " + column_data["column_modifier"] + columns.append(column_line) + m_columns.append(column_data['column_name']) + s_columns.append(column_data['column_sync_name']) + table_name = meta_data[table_id]["name"] + create_statement = "CREATE TABLE IF NOT EXISTS {} ({});".format(table_name, ", ".join(columns)) + drop_statement = 'DROP TABLE IF EXISTS {}'.format(table_name) + select_mariadb_statement = 'SELECT {} FROM {}'.format(', '.join(m_columns), table_name) + select_sqlite_statement = 'SELECT {} FROM {}'.format(', '.join(s_columns), table_name) + insert_sqlite_statement = 'INSERT INTO {}({}) VALUES({})'.format(table_name, ', '.join(s_columns), ', '.join(['?']*len(s_columns))) + insert_mariadb_statement = 'INSERT INTO {}({}) VALUES({})'.format(table_name, ', '.join(m_columns), ', '.join(['?']*len(m_columns))) + truncate_mariadb_statement = 'TRUNCATE {}'.format(table_name) + #logger.debug(create_statement) + #logger.debug(select_mariadb_statement) + table_scripts["create"] = create_statement + table_scripts["drop"] = drop_statement + table_scripts["select_mariadb"] = select_mariadb_statement + table_scripts["select_sqlite"] = select_sqlite_statement + table_scripts["insert_sqlite"] = insert_sqlite_statement + table_scripts["insert_mariadb"] = insert_mariadb_statement + table_scripts["truncate_mariadb"] = truncate_mariadb_statement + table_scripts["count"] = "SELECT COUNT(*) FROM {}".format(table_name) + table_scripts["name"] = table_name + scripts_map[table_id] = table_scripts + return scripts_map diff --git a/kontor-scripts/copy_to_mariadb.py b/kontor-scripts/copy_to_mariadb.py new file mode 100644 index 0000000..059936a --- /dev/null +++ b/kontor-scripts/copy_to_mariadb.py @@ -0,0 +1,52 @@ +""" +copy data from SQLite to MariaDB +""" +import sqlite3 +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + +from config import get_logger, get_database_cursors, get_meta_data, get_scripts + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--recreate-db', action='store_true') +parser.add_argument('--verbose', '-v', action='count', default=0) +args = parser.parse_args() + + +def copy_data(mariadb_conn, sqlite_conn, table_scripts): + mariadb_cursor = mariadb_conn.cursor() + sqlite_cursor = sqlite_conn.cursor() + # logger.info(table_scripts) + for table_id in scripts: + select_statement = scripts[table_id]['select_sqlite'] + # logger.info(select_statement) + insert_statement = scripts[table_id]['insert_mariadb'] + mariadb_cursor.execute("SET FOREIGN_KEY_CHECKS = 0") + mariadb_cursor.execute(scripts[table_id]['truncate_mariadb']) + try: + sqlite_cursor.execute(select_statement) + rows = sqlite_cursor.fetchall() + for row in rows: + try: + mariadb_cursor.execute(insert_statement, row) + except sqlite3.Error as error: + logger.info('insert failed with %s\n%s\n%s', error, insert_statement, row) + mariadb_conn.commit() + mariadb_cursor.execute(scripts[table_id]['count']) + (number_of_rows,) = mariadb_cursor.fetchone() + row = sqlite_cursor.execute(scripts[table_id]['count']).fetchone() + logger.info('%s contains %d : %d entries', scripts[table_id]['name'], number_of_rows, row[0]) + except sqlite3.Error as error: + logger.info('select failed with %s', error) + + +if __name__ == '__main__': + logger = get_logger(args.verbose) + logger.info('kontor.copy_to_sqlite started') + s_conn, m_conn = get_database_cursors(logger) + meta_data_tables = get_meta_data(m_conn) + # logger.info(meta_data_tables) + scripts = get_scripts(meta_data_tables, logger) + copy_data(m_conn, s_conn, scripts) + s_conn.close() + m_conn.close() + logger.info('kontor.copy_to_sqlite finished') diff --git a/kontor-scripts/copy_to_sqlite.py b/kontor-scripts/copy_to_sqlite.py new file mode 100644 index 0000000..3455cb4 --- /dev/null +++ b/kontor-scripts/copy_to_sqlite.py @@ -0,0 +1,51 @@ +""" +copy data from MariaDB to SQLite +""" +import sqlite3 +import mariadb +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from config import get_database_cursors, create_tables, get_logger, get_meta_data, get_scripts + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--recreate-db', action='store_true') +parser.add_argument('--verbose', '-v', action='count', default=0) +args = parser.parse_args() + + +def copy_data(mariadb_conn, sqlite_conn, table_scripts): + mariadb_cursor = mariadb_conn.cursor() + sqlite_cursor = sqlite_conn.cursor() + # logger.info(table_scripts) + for table_id in table_scripts: + select_statement = scripts[table_id]['select_mariadb'] + # logger.info(select_statement) + insert_statement = scripts[table_id]['insert_sqlite'] + try: + mariadb_cursor.execute(select_statement) + rows = mariadb_cursor.fetchall() + for row in rows: + try: + sqlite_cursor.execute(insert_statement, row) + except sqlite3.Error as error: + logger.info('insert failed with %s\n%s\n%s', error, insert_statement, row) + sqlite_conn.commit() + mariadb_cursor.execute(scripts[table_id]['count']) + (number_of_rows,) = mariadb_cursor.fetchone() + row = sqlite_cursor.execute(scripts[table_id]['count']).fetchone() + logger.info('%s contains %d : %d entries', scripts[table_id]['name'], number_of_rows, row[0]) + except mariadb.Error as error: + logger.info('select failed with %s', error) + + +if __name__ == '__main__': + logger = get_logger(args.verbose) + logger.info('kontor.copy_to_sqlite started') + s_conn, m_conn = get_database_cursors(logger) + meta_data_tables = get_meta_data(m_conn) + # logger.info(meta_data_tables) + scripts = get_scripts(meta_data_tables, logger) + create_tables(s_conn, logger, args.recreate_db, scripts) + copy_data(m_conn, s_conn, scripts) + s_conn.close() + m_conn.close() + logger.info('kontor.copy_to_sqlite finished') diff --git a/kontor-scripts/db_structure.py b/kontor-scripts/db_structure.py new file mode 100644 index 0000000..947ab4a --- /dev/null +++ b/kontor-scripts/db_structure.py @@ -0,0 +1,67 @@ +""" +Prints the database kontor structure +""" +import mariadb +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from config import get_database_cursors, get_logger + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +args = parser.parse_args() + + +def show_tables(cur, log): + """ + Retrieves the list of tables from the database + :param cur: + :param log: + :return: + """ + log.info('get list of tables') + table_list = [] + cur.execute("SHOW TABLES") + for (tablename,) in cur.fetchall(): + table_list.append(tablename) + return table_list + + +def get_field_info(cur): + """ + Retrieves the field info associated with a cursor + :param cur: + :return: + """ + field_info = mariadb.fieldinfo() + field_info_text_list = [] + for column in cur.description: + column_name = column[0] + column_type = field_info.type(column) + column_flags = field_info.flag(column) + field_info_text_list.append(f"{column_name}: {column_type} {column_flags}") + return field_info_text_list + + +def get_table_field_info(cur, tablename): + """ + Retrieves the field info associated with a table + :param cur: + :param tablename: + :return: + """ + cur.execute(f"SELECT * FROM {tablename} LIMIT 1") + field_info = get_field_info(cur) + return field_info + + +if __name__ == '__main__': + logger = get_logger(args.verbose) + logger.info("kontor.db_structure started") + _, mariadb_conn = get_database_cursors(logger) + tables = show_tables(mariadb_conn.cursor(), logger) + for table in tables: + field_info_text = get_table_field_info(mariadb_conn.cursor(), table) + print(f"Columns in table {table}:") + print("\n".join(field_info_text)) + print("\n") + mariadb_conn.close() + logger.info("kontor.db_structure finished") diff --git a/kontor-scripts/download.py b/kontor-scripts/download.py new file mode 100644 index 0000000..2faa422 --- /dev/null +++ b/kontor-scripts/download.py @@ -0,0 +1,122 @@ +""" +download files with URLs from DB +""" +import re +import subprocess +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from datetime import datetime +from enum import Enum, auto +from pathlib import Path +from uuid import UUID + +import requests +from config import get_logger + + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +parser.add_argument('--dir', '-d', default='/data/media') +parser.add_argument('--tool', '-t', default='yt-dlp') +parser.add_argument('--dry-run', '-m', action='store_true') +args = parser.parse_args() + +class FileStatus(Enum): + DOWNLOADED = auto() + RENAMED = auto() + UNKNOWN = auto() + +def download_file(url: str, file_info: dict, download_dir: str = "/data/media", dl_tool: str = "yt-dlp") -> dict: + print(f"download file for {url} to {download_dir}") + result = subprocess.run([dl_tool, url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + file_name = __parse_output__(lines_list) + if file_name is None: + file_info['review'] = True + file_info['should_download'] = True + file_info['file_name'] = None + else: + download_file_name = Path(download_dir, file_name) + file_info['should_download'] = False + file_info['file_name'] = download_file_name.name + file_info['cloud_link'] = str(download_file_name.absolute()) + file_info['last_modified_date'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return file_info + + +def __parse_output__(lines_list: list[str]) -> str | None: + file_name = None + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + file_name = line[-file_len:] + return file_name + + +def is_file_downloaded(media_file: dict, dir: Path) -> FileStatus: + file_name_as_title = f"{media_file['file_name']}" + file_title = Path(dir, f"{file_name_as_title}.mp4") + if file_title.exists(): + log.info(f"{file_name_as_title} has been downloaded") + media_file['should_download'] = False + return FileStatus.DOWNLOADED + file_name_as_id = f"{media_file['id']}" + file_with_id_as_name = Path(dir, f"{file_name_as_id}.mp4") + if file_with_id_as_name.exists(): + log.info(f"{file_with_id_as_name} has been downloaded and renamed") + media_file['cloud_link'] = file_with_id_as_name + media_file['should_download'] = False + return FileStatus.RENAMED + log.info("could not find file - start download") + return FileStatus.UNKNOWN + + +def update_status(item_id: UUID, file_info: dict): + update = requests.put(f"http://127.0.0.1:8800/media/files/{item_id}", json=file_info) + log.info(f"update status: {update.status_code}") + log.info(f"update result: {update.json()}") + + +def rename_file(file_info: dict): + item_id = file_info['id'] + file = Path(args.dir, file_info['file_name']) + new_file_path = file.with_name(f"{item_id}{file.suffix}") + log.info(f"rename {file} to {new_file_path}") + file.rename(Path(new_file_path)) + file_info['cloud_link'] = str(new_file_path) + + +if __name__ == '__main__': + log = get_logger(args.verbose, args.config) + log.info('kontor.download started') + response = requests.get("http://127.0.0.1:8800/media/files?download=true") + log.info(f"Status: {response.status_code}") + data = response.json() + log.info(f"data: {len(data)}") + for item in data: + link = item['url'] + file_id = item['id'] + log.info(f"{file_id} - {link}") + download_status: FileStatus = is_file_downloaded(item, args.dir) + match download_status: + case FileStatus.DOWNLOADED: + rename_file(item) + update_status(file_id, item) + case FileStatus.RENAMED: + log.info("update status") + update_status(file_id, item) + case FileStatus.UNKNOWN: + download_file(link, item) + rename_file(item) + log.info(f'{item}') + update_status(file_id, item) + log.info('kontor.download finished') + diff --git a/kontor-scripts/export.py b/kontor-scripts/export.py new file mode 100644 index 0000000..e5ede4f --- /dev/null +++ b/kontor-scripts/export.py @@ -0,0 +1,43 @@ +""" +import data from json file to MariaDB +""" +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + +import yaml +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from platformdirs import PlatformDirs +from pathlib import Path + +from schema.base import Base +from schema.database import KontorDB +from config import get_logger +from schema.database import ExportType + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +parser.add_argument('--file', '-f', default='data.json') +args = parser.parse_args() + + +if __name__ == '__main__': + logger = get_logger(args.verbose, args.config) + logger.info('kontor.export started') + dirs = PlatformDirs(args.config) + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'] + )) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + kontor_db = KontorDB(engine, logger) + kontor_db.export_db(ExportType.JSON, args.file) + logger.info('kontor.export finished') diff --git a/kontor-scripts/import.py b/kontor-scripts/import.py new file mode 100644 index 0000000..f50d82d --- /dev/null +++ b/kontor-scripts/import.py @@ -0,0 +1,42 @@ +""" +import data from json file to MariaDB +""" +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter + +import yaml +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from platformdirs import PlatformDirs +from pathlib import Path + +from schema import Base, KontorDB +from config import get_logger + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +parser.add_argument('--recreate-db', action='store_true') +parser.add_argument('--file', '-f', default='~/data.json') +args = parser.parse_args() + + +if __name__ == '__main__': + logger = get_logger(args.verbose, args.config) + logger.info('kontor.import started') + dirs = PlatformDirs(args.config) + database_config = Path(dirs.user_config_dir, 'database-config.yaml') + with open(database_config, 'rt') as f: + db_config = yaml.safe_load(f.read()) + connect_string = ('mariadb+mariadbconnector://{}:{}@{}:{}/{}'.format( + db_config['mariadb']['user'], + db_config['mariadb']['password'], + db_config['mariadb']['host'], + db_config['mariadb']['port'], + db_config['mariadb']['database'] + )) + engine = create_engine(connect_string) + Base.metadata.create_all(bind=engine, checkfirst=True) + __session__ = sessionmaker(bind=engine) + kontor_db = KontorDB(engine, logger) + kontor_db.import_db(args.file) + logger.info('kontor.import finished') diff --git a/kontor-scripts/json_to_mariadb.py b/kontor-scripts/json_to_mariadb.py new file mode 100644 index 0000000..db3916b --- /dev/null +++ b/kontor-scripts/json_to_mariadb.py @@ -0,0 +1,58 @@ +""" +copy data from JSON to MariaDB +""" +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from pathlib import Path +from config import get_logger, get_database_cursors +import mariadb +import json + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +parser.add_argument('--file', '-f', default='~/.sync/media/data.json') +args = parser.parse_args() + +def copy_data(mariadb_conn, data_file: Path, log): + mariadb_cursor = mariadb_conn.cursor() + import_file = Path(data_file) + if not import_file.exists(): + log.info(f"File {data_file} does not exist. Do nothing.") + return + log.info("read json file") + with open(data_file, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + log.info(f"{table}: {len(json_load[table])}") + # result[table] = import_table(table, json_load[table]) + truncate_statement = 'TRUNCATE {}'.format(table) + #log.info(f"truncate: {truncate_statement}") + mariadb_cursor.execute("SET FOREIGN_KEY_CHECKS = 0") + mariadb_cursor.execute(truncate_statement) + items = json_load[table] + for item in items: + #log.info(f"item: {item}") + values = [] + columns = [] + for (key, value) in item.items(): + columns.append(key) + values.append(value) + row = tuple(values) + log.info(f"values: {row}") + insert_statement = 'INSERT INTO {}({}) VALUES({})'.format(table, ', '.join(columns), ', '.join(['?']*len(columns))) + #log.info(f"statement: {insert_statement}") + mariadb_cursor.execute(insert_statement, row) + try: + mariadb_conn.commit() + except mariadb.Error as error: + log.info('insert failed with %s', error) + + +if __name__ == '__main__': + logger = get_logger(args.verbose, args.config) + logger.info('kontor.json_to_mariadb started') + _, m_conn = get_database_cursors(logger, args.config) + copy_data(m_conn, args.file, logger) + m_conn.close() + logger.info('kontor.json_to_mariadb finished') + diff --git a/kontor-scripts/pyproject.toml b/kontor-scripts/pyproject.toml new file mode 100644 index 0000000..ef91428 --- /dev/null +++ b/kontor-scripts/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "kontor-scripts" +version = "0.1.0" +readme = "README.md" +authors = [ + {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"} +] +maintainers = [ + {name = "Thomas Peetz", email = "thomas.peetz@thpeetz.de"} +] +dependencies = [ + "beautifulsoup4>=4.13.4", + "fastapi[standard]>=0.115.12", + "mariadb>=1.1.12", + "pathlib>=1.0.1", + "platformdirs>=4.3.7", + "pyyaml>=6.0.2", + "requests>=2.32.3", + "sqlalchemy>=2.0.40", + "sqlmodel>=0.0.24", +] diff --git a/kontor-scripts/read_list.py b/kontor-scripts/read_list.py new file mode 100644 index 0000000..cc1c4e8 --- /dev/null +++ b/kontor-scripts/read_list.py @@ -0,0 +1,57 @@ +""" +read file with URLs and store in DB +""" +import uuid +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +import datetime + +import mariadb +from setup import get_database_cursors, get_logger, get_scripts, get_meta_data + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('-f', '--links', help='file with links') +parser.add_argument('--verbose', '-v', action='count', default=0) +args = parser.parse_args() + + +def read_links_file(links_file): + with open(links_file, 'r') as input_file: + lines = input_file.readlines() + return lines + + +def add_link_to_db(statement, connection, video_url, log): + entry_id = str(uuid.uuid4()) + current_date_time = datetime.datetime.now() + try: + cur = connection.cursor() + cur.execute(statement, (entry_id, current_date_time, current_date_time, 0, video_url, True, True, None, None, None, None)) + connection.commit() + log.info(f'link {video_url} added to db') + except mariadb.Error as insert_error: + log.debug("insert failed with %s", insert_error) + entry_id = None + return entry_id + + +if __name__ == '__main__': + logger = get_logger(args.verbose) + logger.info('kontor.read_list started') + s_conn, m_conn = get_database_cursors(logger) + meta_data_tables = get_meta_data(m_conn) + scripts = get_scripts(meta_data_tables, logger) + tables = {} + for table_id in scripts: + tables[scripts[table_id]['name']] = table_id + media_file_id = tables['media_file'] + insert_statement = scripts[tables['media_file']]['insert_mariadb'] + if args.links: + logger.info("read links from file") + links = read_links_file(args.links) + for link in links: + logger.info("add link to db") + add_link_to_db(insert_statement, m_conn, link.strip(), logger) + else: + logger.info('script used: {}'.format(insert_statement)) + logger.info('kontor.read_list finished') + diff --git a/kontor-scripts/requirements.txt b/kontor-scripts/requirements.txt new file mode 100644 index 0000000..9c7bc24 --- /dev/null +++ b/kontor-scripts/requirements.txt @@ -0,0 +1,10 @@ +mariadb +sqlalchemy +pathlib +platformdirs +pyyaml +beautifulsoup4 +sqlmodel +requests +fastapi[standard] + diff --git a/kontor-scripts/schema/__init__.py b/kontor-scripts/schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kontor-scripts/schema/admin.py b/kontor-scripts/schema/admin.py new file mode 100644 index 0000000..f3f6044 --- /dev/null +++ b/kontor-scripts/schema/admin.py @@ -0,0 +1,78 @@ +from datetime import datetime + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship, mapped_column, Mapped + +from .base import Base, BaseMixin + + +class Profile(Base, BaseMixin): + __tablename__ = 'profile' + first_name = Column(String(255)) + last_name = Column(String(255)) + user_name = Column(String(255), nullable=False) + email = Column(String(255)) + password = Column(String(255)) + enabled = Column(BIT(1)) + assignments = relationship("Assignment") + tokens = relationship("Token") + + def get_full_name(self) -> str: + full_name = "" + if self.first_name is not None: + full_name += self.first_name + if self.last_name is not None: + if len(full_name) > 0: + full_name += " " + full_name += self.last_name + return full_name + + +class Token(Base, BaseMixin): + __tablename__ = "token" + token = Column(String(255), nullable=False, unique=True) + name = Column(String(255)) + last_used_date: Mapped[datetime] = mapped_column() + enabled = Column(BIT(1)) + profile_id = Column(String(255), ForeignKey("profile.id"), nullable=False) + profile = relationship("Profile", back_populates="tokens") + + +class Permission(Base, BaseMixin): + __tablename__ = "permission" + name = Column(String(255), nullable=False) + assignments = relationship("Assignment") + + +class Assignment(Base, BaseMixin): + __tablename__ = "assignment" + profile_id = Column(String, ForeignKey("profile.id"), nullable=False) + profile = relationship("Profile", back_populates="assignments") + permission_id = Column(String, ForeignKey("permission.id"), nullable=False) + permission = relationship("Permission", back_populates="assignments") + + +class ModuleData(Base, BaseMixin): + __tablename__ = "module_data" + module_name = Column(String(255), nullable=False) + import_data = Column(BIT(1)) + + +class MailAccount(Base, BaseMixin): + __tablename__ = "mail_account" + host = Column(String(255)) + port = Column(Integer) + protocol = Column(String(255)) + user_name = Column(String(255)) + password = Column(String(255)) + start_tls = Column(BIT(1)) + + +class Mail(Base, BaseMixin): + __tablename__ = "mail" + folder: Mapped[str] = mapped_column() + subject: Mapped[str] = mapped_column() + body: Mapped[str] = mapped_column() + sent_date: Mapped[datetime] = mapped_column() + received_date: Mapped[datetime] = mapped_column() diff --git a/kontor-scripts/schema/base.py b/kontor-scripts/schema/base.py new file mode 100644 index 0000000..4a354e7 --- /dev/null +++ b/kontor-scripts/schema/base.py @@ -0,0 +1,31 @@ +import uuid +from datetime import datetime + +from sqlalchemy import func, Column, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(DeclarativeBase): + pass + + +class BaseMixin: + id = Column(String(255), primary_key=True, default=uuid.uuid4()) + # id: Mapped[str] = mapped_column(primary_key=True, default=uuid.uuid4()) + # created_date = Column(DateTime) + created_date: Mapped[datetime] = mapped_column(default=func.now()) + # last_modified_date = Column(DateTime) + last_modified_date: Mapped[datetime] = mapped_column(default=func.now()) + # version = Column(Integer) + version: Mapped[int] = mapped_column(default=0) + + +class BaseVideoMixin: + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(BIT(1)) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(BIT(1)) diff --git a/kontor-scripts/schema/bookshelf.py b/kontor-scripts/schema/bookshelf.py new file mode 100644 index 0000000..91e0ae4 --- /dev/null +++ b/kontor-scripts/schema/bookshelf.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Article(Base, BaseMixin): + __tablename__ = 'article' + title = Column(String(length=255), unique=True) + article_authors = relationship("ArticleAuthor") + + +class Author(Base, BaseMixin): + __tablename__ = 'author' + first_name = Column(String(255)) + last_name = Column(String(255)) + article_authors = relationship("ArticleAuthor") + book_authors = relationship("BookAuthor") + + +class BookshelfPublisher(Base, BaseMixin): + __tablename__ = 'bookshelf_publisher' + name = Column(String(length=255), unique=True) + books = relationship("Book") + + +class Book(Base, BaseMixin): + __tablename__ = 'book' + isbn = Column(String(255), unique=True) + title = Column(String(255)) + year = Column(Integer, nullable=False) + publisher_id = Column(String, ForeignKey('bookshelf_publisher.id'), nullable=False) + publisher = relationship('BookshelfPublisher', back_populates="books") + book_authors = relationship("BookAuthor") + + +class ArticleAuthor(Base, BaseMixin): + __tablename__ = 'article_author' + article_id = Column(String, ForeignKey('article.id'), nullable=False) + article = relationship('Article', back_populates="article_authors") + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="article_authors") + + +class BookAuthor(Base, BaseMixin): + __tablename__ = 'book_author' + author_id = Column(String, ForeignKey('author.id'), nullable=False) + author = relationship('Author', back_populates="book_authors") + book_id = Column(String, ForeignKey('book.id'), nullable=False) + book = relationship('Book', back_populates="book_authors") diff --git a/kontor-scripts/schema/comic.py b/kontor-scripts/schema/comic.py new file mode 100644 index 0000000..1052d79 --- /dev/null +++ b/kontor-scripts/schema/comic.py @@ -0,0 +1,100 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Publisher(Base, BaseMixin): + __tablename__ = "publisher" + name = Column(String(length=255), unique=True) + comics = relationship("Comic") + + def __repr__(self): + return f'Publisher({self.id} {self.name})' + + def __str__(self): + return self.__repr__() + + +class Comic(Base, BaseMixin): + __tablename__ = 'comic' + title = Column(String(length=255), unique=True) + publisher_id = Column(String, ForeignKey('publisher.id'), nullable=False) + publisher = relationship("Publisher", back_populates="comics") + current_order = Column(BIT(1)) + completed = Column(BIT(1)) + issues = relationship("Issue") + story_arcs = relationship("StoryArc") + trade_paperbacks = relationship("TradePaperback") + volumes = relationship("Volume") + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Comic({self.id} {self.version} {self.title} {self.publisher.name})' + + def __str__(self): + return f'{self.title}({self.id})' + + +class Volume(Base, BaseMixin): + __tablename__ = "volume" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="volumes") + issues = relationship("Issue") + + +class TradePaperback(Base, BaseMixin): + __tablename__ = "trade_paperback" + name = Column(String(length=255), nullable=False) + issue_start = Column(Integer) + issue_end = Column(Integer) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="trade_paperbacks") + + +class StoryArc(Base, BaseMixin): + __tablename__ = "story_arc" + name = Column(String(length=255), nullable=False) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="story_arcs") + + +class Issue(Base, BaseMixin): + __tablename__ = "issue" + issue_number = Column(String(255)) + in_stock = Column(BIT(1)) + is_read = Column(BIT(1)) + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="issues") + volume_id = Column(String, ForeignKey("volume.id"), nullable=True) + volume = relationship("Volume", back_populates="issues") + + +class Artist(Base, BaseMixin): + __tablename__ = "artist" + name = Column(String(length=255), nullable=False) + comic_works = relationship("ComicWork") + + +class WorkType(Base, BaseMixin): + __tablename__ = "worktype" + name = Column(String(length=255), nullable=False, unique=True) + comic_works = relationship("ComicWork") + + def __repr__(self): + return f'Worktype({self.id} {self.version} {self.name} {len(self.comic_works)})' + + def __str__(self): + return f'{self.name}({self.id})' + + +class ComicWork(Base, BaseMixin): + __tablename__ = "comic_work" + comic_id = Column(String, ForeignKey("comic.id"), nullable=False) + comic = relationship("Comic", back_populates="comic_works") + artist_id = Column(String, ForeignKey("artist.id"), nullable=False) + artist = relationship("Artist", back_populates="comic_works") + work_type_id = Column(String, ForeignKey("worktype.id"), nullable=False) + work_type = relationship("WorkType", back_populates="comic_works") diff --git a/kontor-scripts/schema/database.py b/kontor-scripts/schema/database.py new file mode 100644 index 0000000..3e7db0f --- /dev/null +++ b/kontor-scripts/schema/database.py @@ -0,0 +1,399 @@ +import json +import uuid +from datetime import datetime +from enum import Enum, auto +from logging import Logger +from pathlib import Path +from typing import Any + +from sqlalchemy import UUID, select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker + +from .tysc import Card, CardSet, Rooster, Team, FieldPosition, Player, Vendor, Sport +from .comic import Issue, TradePaperback, StoryArc, Volume, ComicWork, Artist, Comic, Publisher, WorkType +from .bookshelf import ArticleAuthor, BookAuthor, BookshelfPublisher, Article, Book, Author +from .admin import Mail, MailAccount, ModuleData, Permission, Profile, Token, Assignment +from .metadata import MetaDataTable, MetaDataColumn +from .media import MediaVideo, MediaArticle, MediaFile, MediaActor, MediaActorFile + + +class ColumnEntry(Enum): + COLUMN_NAME = 'column' + COLUMN_LABEL = 'label' + COLUMN_ORDER = 'order' + COLUMN_REF_COLUMN = 'ref_column' + COLUMN_TYPE = 'type' + COLUMN_WIDGET = 'widget' + + +class StatusType(Enum): + UNKNOWN = auto() + FILE_NAME = auto() + FILE_ID = auto() + DUPLICATE = auto() + CLOUD_LINK = auto() + CLOUD_LINK_ID = auto() + + +class ExportType(Enum): + JSON = "JSON" + YAML = "YAML" + SQLITE = "SQLite" + + +class KontorDB: + + def __init__(self, db_engine: Any, log: Logger): + self.engine = db_engine + self.registry = {} + self.init_registry() + self.log = log + + def init_registry(self): + self.registry[Card.__tablename__] = Card + self.registry[CardSet.__tablename__] = CardSet + self.registry[Rooster.__tablename__] = Rooster + self.registry[Team.__tablename__] = Team + self.registry[FieldPosition.__tablename__] = FieldPosition + self.registry[Player.__tablename__] = Player + self.registry[Vendor.__tablename__] = Vendor + self.registry[Sport.__tablename__] = Sport + self.registry[Issue.__tablename__] = Issue + self.registry[TradePaperback.__tablename__] = TradePaperback + self.registry[StoryArc.__tablename__] = StoryArc + self.registry[Volume.__tablename__] = Volume + self.registry[ComicWork.__tablename__] = ComicWork + self.registry[Artist.__tablename__] = Artist + self.registry[Comic.__tablename__] = Comic + self.registry[Publisher.__tablename__] = Publisher + self.registry[WorkType.__tablename__] = WorkType + self.registry[ArticleAuthor.__tablename__] = ArticleAuthor + self.registry[BookAuthor.__tablename__] = BookAuthor + self.registry[BookshelfPublisher.__tablename__] = BookshelfPublisher + self.registry[Article.__tablename__] = Article + self.registry[Book.__tablename__] = Book + self.registry[Author.__tablename__] = Author + self.registry[MediaFile.__tablename__] = MediaFile + self.registry[MediaActor.__tablename__] = MediaActor + self.registry[MediaActorFile.__tablename__] = MediaActorFile + self.registry[MediaArticle.__tablename__] = MediaArticle + self.registry[MediaVideo.__tablename__] = MediaVideo + self.registry[MetaDataColumn.__tablename__] = MetaDataColumn + self.registry[MetaDataTable.__tablename__] = MetaDataTable + self.registry[Assignment.__tablename__] = Assignment + self.registry[Token.__tablename__] = Token + self.registry[Profile.__tablename__] = Profile + self.registry[Permission.__tablename__] = Permission + self.registry[ModuleData.__tablename__] = ModuleData + self.registry[MailAccount.__tablename__] = MailAccount + self.registry[Mail.__tablename__] = Mail + + def get_table_names(self) -> list: + result = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + tables = session.scalars(select(MetaDataTable)).all() + result = [table.table_name for table in tables] + return result + + def get_table_by_name(self, table_name: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + _filter = {'table_name': table_name} + with __session__() as session: + table = session.query(MetaDataTable).filter_by(**_filter).one() + result['id'] = table.id + result['table_name'] = table.table_name + return result + + def get_column_meta_data(self, table_name: str, view_only=True) -> dict: + meta_data = {} + order = 0 + __session__ = sessionmaker(self.engine) + columns = list() + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + if view_only: + _filters['is_shown'] = True + with __session__() as session: + columns = session.query(MetaDataColumn).filter_by(**_filters).all() + for column in columns: + # self.log.info("get_column_meta_data: %s %s %d", column.column_name, column.column_label, column.column_order) + meta_data[order] = { + ColumnEntry.COLUMN_NAME: column.column_name, + ColumnEntry.COLUMN_LABEL: column.column_label, + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_REF_COLUMN: column.ref_column, + ColumnEntry.COLUMN_TYPE: column.column_type + } + order += 1 + return meta_data + + def get_columns(self, table_name: str) -> dict: + columns = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id']} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + columns[column.column_name] = { + ColumnEntry.COLUMN_ORDER: column.column_order, + ColumnEntry.COLUMN_TYPE: column.column_type + } + return columns + + def get_filters(self, table_name: str) -> dict: + _filter_map = {} + __session__ = sessionmaker(self.engine) + table_info = self.get_table_by_name(table_name) + _filters = {'table_id': table_info['id'], 'show_filter': True} + with __session__() as session: + for column in session.query(MetaDataColumn).filter_by(**_filters).all(): + _filter_map[column.column_name] = { + ColumnEntry.COLUMN_LABEL: column.filter_label, + ColumnEntry.COLUMN_WIDGET: None + } + return _filter_map + + def data(self, table_name: str, columns: dict, filters: dict) -> list: + data = [] + __session__ = sessionmaker(self.engine) + table = self.registry[table_name] + with __session__() as session: + entries = [] + if len(filters) == 0: + entries = session.scalars(select(table)).all() + else: + entries = session.scalars(select(table).filter_by(**filters)).all() + for entry in entries: + # self.log.info("data: %s", entry) + row = [] + for order in columns.keys(): + column_name = columns[order][ColumnEntry.COLUMN_NAME] + ref_column = columns[order][ColumnEntry.COLUMN_REF_COLUMN] + if str(column_name).endswith("_id"): + ref_table = column_name[:-3] + ref = getattr(entry, ref_table) + value = getattr(ref, ref_column) + row.append(value) + else: + row.append(getattr(entry, column_name)) + data.append(row) + # self.log.info("data: %s", data) + return data + + def export_db(self, export_type: ExportType, export_file_name: str) -> dict: + results = {} + db = {} + export_table_list = self.get_table_names() + for table in export_table_list: + columns = self.get_column_meta_data(table, view_only=False) + if table in self.registry: + model = self.registry[table] + else: + self.log.info(f"table {table} is not registered") + continue + __session__ = sessionmaker(self.engine) + with __session__() as session: + rows = session.query(model).all() + entries = [] + for row in rows: + # print(row) + entry = {} + for order in columns: + # print(columns[order]) + column_name = columns[order][ColumnEntry.COLUMN_NAME] + # print(f"get value {column_name} from {row} of table {table}") + try: + value = getattr(row, column_name) + if isinstance(value, datetime): + entry[column_name] = str(value) + else: + entry[column_name] = value + except AttributeError: + pass + entries.append(entry) + db[table] = entries + results[table] = len(entries) + match export_type: + case ExportType.JSON: + json_dump = json.dumps(db, indent=4) + with open(export_file_name, "w") as dump_file: + dump_file.write(json_dump) + case ExportType.YAML: + pass + case ExportType.SQLITE: + pass + self.log.info(f"{len(results)} tables exported") + return results + + def import_db(self, import_file_name: str) -> dict: + result = {} + import_file = Path(import_file_name) + if not import_file.exists(): + self.log.info(f"File {import_file_name} does not exist. Do nothing.") + return result + match import_file.suffix: + case '.json': + print("read json file") + with open(import_file_name, 'r') as json_file: + json_load = json.load(json_file) + for table in json_load: + self.log.info(f"{table}: {len(json_load[table])}") + result[table] = self.import_table(table, json_load[table]) + case '.yml': + print("read yaml file") + case '.yaml': + print("read yaml file") + case '.db': + print("read sqlite file") + return result + + def import_table(self, table_name: str, items: list) -> dict: + result = {} + updated = [] + added = [] + remaining = [] + existing_ids = self.get_ids(table_name) + self.log.info(f"found {len(existing_ids)} existing ids for table {table_name}") + for item in items: + current_id = item['id'] + # print(f"import item: {item}") + found_item = None + __session__ = sessionmaker(self.engine) + with __session__() as session: + found_item = session.get(self.registry[table_name], current_id) + # print(f"found item: {found_item}") + if found_item is not None: + changed = self.update_entry(table_name, current_id, item) + updated.append(item) + if changed: + self.log.info(f"{current_id} has changed") + updated.append(item) + existing_ids.remove(current_id) + else: + try: + self.add_entry(table_name, item) + added.append(item) + except IntegrityError as error: + self.log.info(f"Could not add item, due to: {error.detail}") + if len(existing_ids) > 0: + print(f"remaining items for {table_name}: {existing_ids}") + remaining.extend(existing_ids) + result['updated'] = updated + result['added'] = added + result['remaining'] = remaining + return result + + def get_ids(self, table_name: str) -> list: + existing_ids = [] + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(self.registry[table_name]).all() + for item in items: + existing_ids.append(getattr(item, 'id')) + return existing_ids + + def add_entry(self, table_name: str, update_item: dict): + self.log.debug(f"add entry to table {table_name} with {update_item}") + __session__ = sessionmaker(self.engine) + with __session__() as session: + add_item = self.registry[table_name]() + for key in update_item.keys(): + update_value = update_item[key] + setattr(add_item, key, update_value) + session.add(add_item) + session.commit() + + def update_entry(self, table_name, current_id, update_item: dict) -> bool: + # self.log.info("update entry to table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + existing_item = session.query(self.registry[table_name]).get(current_id) + changed = False + for key in update_item.keys(): + update_value = update_item[key] + existing_value = getattr(existing_item, key) + if type(existing_value) is not type(update_value): + existing_value = str(existing_value) + if existing_value != update_value: + self.log.info(f"{key} has changed: {existing_value} != {update_value}") + setattr(existing_item, key, update_value) + session.commit() + changed = True + self.log.info(f"update {key} with {update_value}") + return changed + + def add_link(self, link: str) -> dict: + result = {} + __session__ = sessionmaker(self.engine) + with __session__() as session: + media_file = MediaFile() + media_file.id = str(uuid.uuid4()) + media_file.created_date = datetime.now() + media_file.last_modified_date = datetime.now() + media_file.version = 0 + media_file.url = link + media_file.review = 1 + media_file.should_download = 1 + try: + session.add(media_file) + session.commit() + result['added'] = {'url': media_file.url, 'title': media_file.title, 'review': media_file.review, + 'download': media_file.should_download} + except IntegrityError as error: + session.rollback() + result['error'] = error.orig + return result + + def update_titles(self) -> dict: + update_list = {} + __session__ = sessionmaker(self.engine) + _filter = {'review': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + self.log.info("%d entries found for updating titles", len(links)) + for link in links: + url = link.url + if url is None: + continue + link.update_title() + session.commit() + update_list[link.id] = link.title + return update_list + + def get_download_list(self) -> list[UUID]: + download_list = [] + __session__ = sessionmaker(self.engine) + _filter = {'should_download': True} + with __session__() as session: + links = session.query(MediaFile).filter_by(**_filter).all() + for link in links: + url = link.url + if url is None: + continue + download_list.append(link.id) + return download_list + + def download_file(self, entry_id: str, download_dir="/data/media", dl_tool="yt-dlp") -> str: + __session__ = sessionmaker(self.engine) + with __session__() as session: + link = session.query(MediaFile).get(entry_id) + link.download_file(download_dir, dl_tool) + session.commit() + file_name = link.file_name + return file_name + + def delete_entries(self): + for (table_name, table) in self.registry.items(): + # self.log.info("delete entries from table %s", table_name) + __session__ = sessionmaker(self.engine) + with __session__() as session: + items = session.query(table).all() + for item in items: + session.delete(item) + session.commit() + + def check_files(self): + pass diff --git a/kontor-scripts/schema/media.py b/kontor-scripts/schema/media.py new file mode 100644 index 0000000..5dcc5c6 --- /dev/null +++ b/kontor-scripts/schema/media.py @@ -0,0 +1,99 @@ +import re +import subprocess +from datetime import datetime +from pathlib import Path + +import requests +from bs4 import BeautifulSoup +from sqlalchemy import Boolean, Column, False_, String, ForeignKey +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin, BaseVideoMixin + + +class MediaFile(Base, BaseMixin, BaseVideoMixin): + __tablename__ = 'media_file' + media_actor_files = relationship("MediaActorFile") + + def __repr__(self): + return f'MediaFile({self.id} {self.title} {self.title})' + + def __str__(self): + return f'{self.title}({self.id})' + + def update_title(self) -> None: + print(f"update title for {self.url}") + try: + r = requests.get(self.url) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + self.title = title + self.review = False_ + except: + self.title = None + self.review = True + self.last_modified_date = datetime.now() + + def download_file(self, download_dir: str, dl_tool: str): + print(f"download file for {self.url} to {download_dir}") + result = subprocess.run([dl_tool, self.url], cwd=download_dir, capture_output=True, text=True) + if result.returncode == 0: + output = result.stdout + output = re.sub(' +', ' ', output) + lines_list = output.splitlines() + file_name = self.__parse_output__(lines_list) + if file_name is None: + self.review = True + self.should_download = True + self.file_name = None + else: + download_file = Path(file_name) + self.should_download = False_ + self.file_name = download_file.name + self.cloud_link = str(download_file.absolute()) + self.last_modified_date = datetime.now() + + def __parse_output__(self, lines_list): + self.file_name = None + for line in lines_list: + if 'has already been downloaded' in line: + end_len = len(' has already been downloaded') + self.file_name = line[11:-end_len] + if 'Destination' in line: + line_len = len(line) + start_len = len('[download] Destination: ') + file_len = line_len - start_len + self.file_name = line[-file_len:] + return self.file_name + + +class MediaActor(Base, BaseMixin): + __tablename__ = 'media_actor' + name = Column(String(255)) + media_actor_files = relationship("MediaActorFile") + + +class MediaActorFile(Base, BaseMixin): + __tablename__ = 'media_actor_file' + media_actor_id = Column(String(255), ForeignKey("media_actor.id"), nullable=False) + media_actor = relationship("MediaActor", back_populates="media_actor_files") + media_file_id = Column(String(255), ForeignKey("media_file.id"), nullable=True) + media_file = relationship("MediaFile", back_populates="media_actor_files") + + +class MediaArticle(Base, BaseMixin): + __tablename__ = 'media_article' + review = Column(Boolean) + title = Column(String(255)) + url = Column(String(255), unique=True) + + +class MediaVideo(Base, BaseMixin): + __tablename__ = 'media_video' + cloud_link = Column(String(255)) + file_name = Column(String(255)) + path = Column(String(255)) + review = Column(Boolean) + title = Column(String(255)) + url = Column(String(255), unique=True) + should_download = Column(Boolean) diff --git a/kontor-scripts/schema/metadata.py b/kontor-scripts/schema/metadata.py new file mode 100644 index 0000000..950cebe --- /dev/null +++ b/kontor-scripts/schema/metadata.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, String, ForeignKey, Integer +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class MetaDataTable(Base, BaseMixin): + __tablename__ = 'meta_data_table' + table_name = Column(String(255), unique=True) + table_columns = relationship("MetaDataColumn") + + def __repr__(self): + return f'MetaDataTable({self.id} {self.table_name})' + + def __str__(self): + return f'{self.table_name}({self.id})' + + +class MetaDataColumn(Base, BaseMixin): + __tablename__ = 'meta_data_column' + column_name = Column(String(255), nullable=False) + column_sync_name = Column(String(255)) + column_type = Column(String(255)) + column_modifier = Column(String(255), nullable=True) + column_order = Column(Integer) + table_id = Column(String, ForeignKey('meta_data_table.id')) + table = relationship("MetaDataTable", back_populates="table_columns") + column_label = Column(String(255)) + filter_label = Column(String(255)) + is_shown = Column(BIT(1)) + show_filter = Column(BIT(1)) + ref_column = Column(String, nullable=True) + + def __repr__(self): + if self.column_name is None: + return f'MetaDataColumn({self.id} {self.table.table_name}.__)' + else: + return f'MetaDataColumn({self.id} {self.table.table_name}.{self.column_name})' + + def __str__(self): + return f'{self.column_name}({self.id})' diff --git a/kontor-scripts/schema/tysc.py b/kontor-scripts/schema/tysc.py new file mode 100644 index 0000000..32c88f1 --- /dev/null +++ b/kontor-scripts/schema/tysc.py @@ -0,0 +1,100 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.mysql import BIT +from sqlalchemy.orm import relationship + +from .base import Base, BaseMixin + + +class Sport(Base, BaseMixin): + __tablename__ = "sport" + __table_args__ = ( + UniqueConstraint("name"), + ) + name = Column(String(255), nullable=False, index=True, unique=True) + teams = relationship("Team") + positions = relationship("FieldPosition") + + +class Team(Base, BaseMixin): + __tablename__ = "team" + name = Column(String(255), nullable=False, index=True, unique=True) + short_name = Column(String(255), nullable=False, ) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False) + sport = relationship("Sport", back_populates="teams") + roosters = relationship("Rooster") + + +class FieldPosition(Base, BaseMixin): + __tablename__ = "field_position" + __table_args__ = ( + UniqueConstraint("name", "sport_id"), + UniqueConstraint("short_name", "sport_id"), + ) + name = Column(String(255), nullable=False, index=True) + short_name = Column(String(255), nullable=False) + sport_id = Column(String, ForeignKey("sport.id"), nullable=False, index=True) + sport = relationship("Sport", back_populates="positions") + roosters = relationship("Rooster") + + +class Player(Base, BaseMixin): + __tablename__ = "player" + __table_args__ = ( + UniqueConstraint("first_name", "last_name"), + ) + first_name = Column(String(255), nullable=False, index=True) + last_name = Column(String(255), nullable=False, index=True) + roosters = relationship("Rooster") + + def get_full_name(self) -> str: + return f"{self.last_name}, {self.first_name}" + + +class Rooster(Base, BaseMixin): + __tablename__ = "rooster" + __table_args__ = ( + UniqueConstraint("year", "team_id", "player_id", "position_id"), + ) + year = Column(Integer) + team_id = Column(String, ForeignKey("team.id"), nullable=False, index=True) + team = relationship("Team", back_populates="roosters") + player_id = Column(String, ForeignKey("player.id"), nullable=False, index=True) + player = relationship("Player", back_populates="roosters") + position_id = Column(String, ForeignKey("field_position.id"), nullable=False, index=True) + position = relationship("FieldPosition", back_populates="roosters") + cards = relationship("Card") + + +class Vendor(Base, BaseMixin): + __tablename__ = "vendor" + name = Column(String(255), nullable=False, unique=True, index=True) + card_sets = relationship("CardSet") + cards = relationship("Card") + + +class CardSet(Base, BaseMixin): + __tablename__ = "card_set" + __table_args__ = ( + UniqueConstraint("name", "vendor_id"), + ) + name = Column(String(255), index=True) + parallel_set = Column(BIT(1)) + insert_set = Column(BIT(1)) + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False, index=True) + vendor = relationship("Vendor", back_populates="card_sets") + cards = relationship("Card") + + +class Card(Base, BaseMixin): + __tablename__ = "card" + __table_args__ = ( + UniqueConstraint("card_number", "year", "vendor_id", "card_set_id"), + ) + card_number = Column(Integer, index=True) + year = Column(Integer, index=True) + card_set_id = Column(String, ForeignKey("card_set.id"), nullable=False) + card_set = relationship("CardSet", back_populates="cards") + rooster_id = Column(String, ForeignKey("rooster.id"), nullable=False) + rooster = relationship("Rooster", back_populates="cards") + vendor_id = Column(String, ForeignKey("vendor.id"), nullable=False) + vendor = relationship("Vendor", back_populates="cards") diff --git a/kontor-scripts/update_title.py b/kontor-scripts/update_title.py new file mode 100644 index 0000000..a410eac --- /dev/null +++ b/kontor-scripts/update_title.py @@ -0,0 +1,59 @@ +""" +download files with URLs from DB +""" +import logging.config + +import requests +import yaml +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +from pathlib import Path + +from bs4 import BeautifulSoup +from platformdirs import PlatformDirs + +parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('--verbose', '-v', action='count', default=0) +parser.add_argument('--config', '-c', default='kontor-docker') +args = parser.parse_args() + +def get_logger(level: int, config: str): + dirs = PlatformDirs(config) + logging_config = Path(dirs.user_config_dir, 'logging-config.yaml') + with open(logging_config, 'rt') as f: + configDict = yaml.safe_load(f.read()) + logging.config.dictConfig(configDict) + logger = logging.getLogger('development') + if level is not None: + match level: + case 0: + logger.setLevel(logging.INFO) + case 1: + logger.setLevel(logging.DEBUG) + case _: + logger.setLevel(logging.CRITICAL) + return logger + + +if __name__ == '__main__': + log = get_logger(args.verbose, args.config) + log.info('kontor.update_titles started') + response = requests.get("http://127.0.0.1:8800/media/files?review=true") + log.info(f"Status: {response.status_code}") + data = response.json() + log.info(f"data: {len(data)}") + for item in data: + link = item['url'] + log.info(f"{item['id']} - {link}") + try: + r = requests.get(link) + soup = BeautifulSoup(r.content, "html.parser") + title = soup.title.string + item['title'] = title + item['review'] = 0 + except: + item['title'] = None + item['review'] = 1 + update = requests.put(f"http://127.0.0.1:8800/media/files/{item['id']}", json=item) + log.info(f"update status: {update.status_code}") + log.info(f"update result: {update.json()}") + log.info('kontor.update_titles finished') diff --git a/kontor-scripts/uv.lock b/kontor-scripts/uv.lock new file mode 100644 index 0000000..048ada7 --- /dev/null +++ b/kontor-scripts/uv.lock @@ -0,0 +1,699 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload_time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload_time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload_time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload_time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload_time = "2025-01-31T02:16:47.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload_time = "2025-01-31T02:16:45.015Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload_time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload_time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload_time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload_time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload_time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload_time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload_time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload_time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload_time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload_time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload_time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload_time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload_time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload_time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload_time = "2024-12-24T18:12:32.852Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload_time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload_time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967, upload_time = "2024-06-20T11:30:30.034Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521, upload_time = "2024-06-20T11:30:28.248Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload_time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload_time = "2025-03-23T22:55:42.101Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753, upload_time = "2024-12-15T14:28:10.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705, upload_time = "2024-12-15T14:28:06.18Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "greenlet" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/74/907bb43af91782e0366b0960af62a8ce1f9398e4291cac7beaeffbee0c04/greenlet-3.2.1.tar.gz", hash = "sha256:9f4dd4b4946b14bb3bf038f81e1d2e535b7d94f1b2a59fdba1293cd9c1a0a4d7", size = 184475, upload_time = "2025-04-22T14:40:18.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/2a/581b3808afec55b2db838742527c40b4ce68b9b64feedff0fd0123f4b19a/greenlet-3.2.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e1967882f0c42eaf42282a87579685c8673c51153b845fde1ee81be720ae27ac", size = 269119, upload_time = "2025-04-22T14:25:01.798Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f3/1c4e27fbdc84e13f05afc2baf605e704668ffa26e73a43eca93e1120813e/greenlet-3.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e77ae69032a95640a5fe8c857ec7bee569a0997e809570f4c92048691ce4b437", size = 637314, upload_time = "2025-04-22T14:53:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/9fc43cb0044f425f7252da9847893b6de4e3b20c0a748bce7ab3f063d5bc/greenlet-3.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3227c6ec1149d4520bc99edac3b9bc8358d0034825f3ca7572165cb502d8f29a", size = 651421, upload_time = "2025-04-22T14:55:00.852Z" }, + { url = "https://files.pythonhosted.org/packages/8a/65/d47c03cdc62c6680206b7420c4a98363ee997e87a5e9da1e83bd7eeb57a8/greenlet-3.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ddda0197c5b46eedb5628d33dad034c455ae77708c7bf192686e760e26d6a0c", size = 645789, upload_time = "2025-04-22T15:04:37.702Z" }, + { url = "https://files.pythonhosted.org/packages/2f/40/0faf8bee1b106c241780f377b9951dd4564ef0972de1942ef74687aa6bba/greenlet-3.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de62b542e5dcf0b6116c310dec17b82bb06ef2ceb696156ff7bf74a7a498d982", size = 648262, upload_time = "2025-04-22T14:27:07.55Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a8/73305f713183c2cb08f3ddd32eaa20a6854ba9c37061d682192db9b021c3/greenlet-3.2.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c07a0c01010df42f1f058b3973decc69c4d82e036a951c3deaf89ab114054c07", size = 606770, upload_time = "2025-04-22T14:25:58.34Z" }, + { url = "https://files.pythonhosted.org/packages/c3/05/7d726e1fb7f8a6ac55ff212a54238a36c57db83446523c763e20cd30b837/greenlet-3.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2530bfb0abcd451ea81068e6d0a1aac6dabf3f4c23c8bd8e2a8f579c2dd60d95", size = 1117960, upload_time = "2025-04-22T14:59:00.373Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9f/2b6cb1bd9f1537e7b08c08705c4a1d7bd4f64489c67d102225c4fd262bda/greenlet-3.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c472adfca310f849903295c351d297559462067f618944ce2650a1878b84123", size = 1145500, upload_time = "2025-04-22T14:28:12.441Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f6/339c6e707062319546598eb9827d3ca8942a3eccc610d4a54c1da7b62527/greenlet-3.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:24a496479bc8bd01c39aa6516a43c717b4cee7196573c47b1f8e1011f7c12495", size = 295994, upload_time = "2025-04-22T14:50:44.796Z" }, + { url = "https://files.pythonhosted.org/packages/f1/72/2a251d74a596af7bb1717e891ad4275a3fd5ac06152319d7ad8c77f876af/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:175d583f7d5ee57845591fc30d852b75b144eb44b05f38b67966ed6df05c8526", size = 629889, upload_time = "2025-04-22T14:53:48.434Z" }, + { url = "https://files.pythonhosted.org/packages/29/2e/d7ed8bf97641bf704b6a43907c0e082cdf44d5bc026eb8e1b79283e7a719/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ecc9d33ca9428e4536ea53e79d781792cee114d2fa2695b173092bdbd8cd6d5", size = 635261, upload_time = "2025-04-22T14:55:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/802aa27848a6fcb5e566f69c64534f572e310f0f12d41e9201a81e741551/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f56382ac4df3860ebed8ed838f268f03ddf4e459b954415534130062b16bc32", size = 632523, upload_time = "2025-04-22T15:04:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/56/09/f7c1c3bab9b4c589ad356503dd71be00935e9c4db4db516ed88fc80f1187/greenlet-3.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc45a7189c91c0f89aaf9d69da428ce8301b0fd66c914a499199cfb0c28420fc", size = 628816, upload_time = "2025-04-22T14:27:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/1bb90d30b5450eac2dffeaac6b692857c4bd642c21883b79faa8fa056cf2/greenlet-3.2.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51a2f49da08cff79ee42eb22f1658a2aed60c72792f0a0a95f5f0ca6d101b1fb", size = 593687, upload_time = "2025-04-22T14:25:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b5/adbe03c8b4c178add20cc716021183ae6b0326d56ba8793d7828c94286f6/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:0c68bbc639359493420282d2f34fa114e992a8724481d700da0b10d10a7611b8", size = 1105754, upload_time = "2025-04-22T14:59:02.585Z" }, + { url = "https://files.pythonhosted.org/packages/39/93/84582d7ef38dec009543ccadec6ab41079a6cbc2b8c0566bcd07bf1aaf6c/greenlet-3.2.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:e775176b5c203a1fa4be19f91da00fd3bff536868b77b237da3f4daa5971ae5d", size = 1125160, upload_time = "2025-04-22T14:28:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/01/e6/f9d759788518a6248684e3afeb3691f3ab0276d769b6217a1533362298c8/greenlet-3.2.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d6668caf15f181c1b82fb6406f3911696975cc4c37d782e19cb7ba499e556189", size = 269897, upload_time = "2025-04-22T14:27:14.044Z" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload_time = "2022-09-25T15:40:01.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload_time = "2022-09-25T15:39:59.68Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/45/ad3e1b4d448f22c0cff4f5692f5ed0666658578e358b8d58a19846048059/httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad", size = 85385, upload_time = "2025-04-11T14:42:46.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8d/f052b1e336bb2c1fc7ed1aaed898aa570c0b61a09707b108979d9fc6e308/httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be", size = 78732, upload_time = "2025-04-11T14:42:44.896Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload_time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload_time = "2024-10-16T19:44:38.738Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload_time = "2024-10-16T19:44:39.818Z" }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload_time = "2024-10-16T19:44:41.189Z" }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload_time = "2024-10-16T19:44:42.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload_time = "2024-10-16T19:44:43.959Z" }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload_time = "2024-10-16T19:44:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload_time = "2024-10-16T19:44:46.46Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload_time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload_time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload_time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload_time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kontor-scripts" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "fastapi", extra = ["standard"] }, + { name = "mariadb" }, + { name = "pathlib" }, + { name = "platformdirs" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy" }, + { name = "sqlmodel" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.13.4" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, + { name = "mariadb", specifier = ">=1.1.12" }, + { name = "pathlib", specifier = ">=1.0.1" }, + { name = "platformdirs", specifier = ">=4.3.7" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, + { name = "sqlmodel", specifier = ">=0.0.24" }, +] + +[[package]] +name = "mariadb" +version = "1.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/bb/4bbc803fbdafedbfba015f7cc1ab1e87a6d1de36725ba058c53e2f8a45ad/mariadb-1.1.12.tar.gz", hash = "sha256:50b02ff2c78b1b4f4628a054e3c8c7dd92972137727a5cc309a64c9ed20c878c", size = 85934, upload_time = "2025-02-13T13:11:48.642Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/1b/b6eca3870ac1b5577a10d3b49ba42ac263c2e5718c9224cc1c8463940422/mariadb-1.1.12-cp313-cp313-win32.whl", hash = "sha256:ba43c42130d41352f32a5786c339cc931d05472ef7640fa3764d428dc294b88e", size = 184338, upload_time = "2025-02-13T13:11:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ff/c29a543ee1f9009755bc304138f61cd9b0ee1f14533e446513f84ccf143a/mariadb-1.1.12-cp313-cp313-win_amd64.whl", hash = "sha256:b69bc18418e72fcf359d17736cdc3f601a271203aff13ef7c57a415c8fd52ab0", size = 201272, upload_time = "2025-02-13T13:11:38.074Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload_time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload_time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload_time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload_time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload_time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload_time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload_time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload_time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload_time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload_time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload_time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload_time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload_time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload_time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload_time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload_time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload_time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload_time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload_time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload_time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload_time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload_time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload_time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload_time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload_time = "2014-09-03T15:41:57.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload_time = "2022-05-04T13:37:20.585Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload_time = "2025-03-19T20:36:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload_time = "2025-03-19T20:36:09.038Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513, upload_time = "2025-04-08T13:27:06.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload_time = "2025-04-08T13:27:03.789Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395, upload_time = "2025-04-02T09:49:41.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551, upload_time = "2025-04-02T09:47:51.648Z" }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785, upload_time = "2025-04-02T09:47:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758, upload_time = "2025-04-02T09:47:55.006Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109, upload_time = "2025-04-02T09:47:56.532Z" }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159, upload_time = "2025-04-02T09:47:58.088Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222, upload_time = "2025-04-02T09:47:59.591Z" }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980, upload_time = "2025-04-02T09:48:01.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840, upload_time = "2025-04-02T09:48:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518, upload_time = "2025-04-02T09:48:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025, upload_time = "2025-04-02T09:48:06.226Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991, upload_time = "2025-04-02T09:48:08.114Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262, upload_time = "2025-04-02T09:48:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626, upload_time = "2025-04-02T09:48:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590, upload_time = "2025-04-02T09:48:12.861Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963, upload_time = "2025-04-02T09:48:14.553Z" }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896, upload_time = "2025-04-02T09:48:16.222Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810, upload_time = "2025-04-02T09:48:17.97Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload_time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload_time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload_time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload_time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload_time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload_time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload_time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload_time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload_time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload_time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload_time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload_time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload_time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload_time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload_time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload_time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload_time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload_time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/69/e328fb8986814147562b2617f22b06723f60b0c85c85afc0408b9f324a97/rich_toolkit-0.14.3.tar.gz", hash = "sha256:b72a342e52253b912681b027e94226e2deea616494420eec0b09a7219a72a0a5", size = 104469, upload_time = "2025-04-23T14:54:52.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/e0c7b06657ca1d4317d9e37ea5657a88a20dc3507b2ee6939ace0ff9036e/rich_toolkit-0.14.3-py3-none-any.whl", hash = "sha256:2ec72dcdf1bbb09b6a9286a4eddcd4d43369da3b22fe3f28e5a92143618b8ac6", size = 24258, upload_time = "2025-04-23T14:54:51.443Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload_time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload_time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload_time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.40" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload_time = "2025-03-27T17:52:31.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/18/4e3a86cc0232377bc48c373a9ba6a1b3fb79ba32dbb4eda0b357f5a2c59d/sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01", size = 2107887, upload_time = "2025-03-27T18:40:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/cb/60/9fa692b1d2ffc4cbd5f47753731fd332afed30137115d862d6e9a1e962c7/sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705", size = 2098367, upload_time = "2025-03-27T18:40:07.182Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9f/84b78357ca641714a439eb3fbbddb17297dacfa05d951dbf24f28d7b5c08/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364", size = 3184806, upload_time = "2025-03-27T18:51:29.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7d/e06164161b6bfce04c01bfa01518a20cccbd4100d5c951e5a7422189191a/sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0", size = 3198131, upload_time = "2025-03-27T18:50:31.616Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/354af20da42d7ec7b5c9de99edafbb7663a1d75686d1999ceb2c15811302/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db", size = 3131364, upload_time = "2025-03-27T18:51:31.336Z" }, + { url = "https://files.pythonhosted.org/packages/7a/2f/48a41ff4e6e10549d83fcc551ab85c268bde7c03cf77afb36303c6594d11/sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26", size = 3159482, upload_time = "2025-03-27T18:50:33.201Z" }, + { url = "https://files.pythonhosted.org/packages/33/ac/e5e0a807163652a35be878c0ad5cfd8b1d29605edcadfb5df3c512cdf9f3/sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500", size = 2080704, upload_time = "2025-03-27T18:46:00.193Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cb/f38c61f7f2fd4d10494c1c135ff6a6ddb63508d0b47bccccd93670637309/sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad", size = 2104564, upload_time = "2025-03-27T18:46:01.442Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload_time = "2025-03-27T18:40:43.796Z" }, +] + +[[package]] +name = "sqlmodel" +version = "0.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780, upload_time = "2025-03-07T05:43:32.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622, upload_time = "2025-03-07T05:43:30.37Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload_time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload_time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "typer" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711, upload_time = "2025-02-27T19:17:34.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload_time = "2025-02-27T19:17:32.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload_time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload_time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload_time = "2025-02-25T17:27:59.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload_time = "2025-02-25T17:27:57.754Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload_time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload_time = "2025-04-19T06:02:48.42Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload_time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload_time = "2024-10-14T23:38:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload_time = "2024-10-14T23:38:02.309Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload_time = "2024-10-14T23:38:04.711Z" }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload_time = "2024-10-14T23:38:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload_time = "2024-10-14T23:38:08.416Z" }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload_time = "2024-10-14T23:38:10.888Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload_time = "2025-04-08T10:36:26.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/62/435766874b704f39b2fecd8395a29042db2b5ec4005bd34523415e9bd2e0/watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d", size = 401531, upload_time = "2025-04-08T10:35:35.792Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/e52a02c05411b9cb02823e6797ef9bbba0bfaf1bb627da1634d44d8af833/watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763", size = 392417, upload_time = "2025-04-08T10:35:37.048Z" }, + { url = "https://files.pythonhosted.org/packages/3f/53/c4af6819770455932144e0109d4854437769672d7ad897e76e8e1673435d/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40", size = 453423, upload_time = "2025-04-08T10:35:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d1/8e88df58bbbf819b8bc5cfbacd3c79e01b40261cad0fc84d1e1ebd778a07/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563", size = 458185, upload_time = "2025-04-08T10:35:39.708Z" }, + { url = "https://files.pythonhosted.org/packages/ff/70/fffaa11962dd5429e47e478a18736d4e42bec42404f5ee3b92ef1b87ad60/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04", size = 486696, upload_time = "2025-04-08T10:35:41.469Z" }, + { url = "https://files.pythonhosted.org/packages/39/db/723c0328e8b3692d53eb273797d9a08be6ffb1d16f1c0ba2bdbdc2a3852c/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f", size = 522327, upload_time = "2025-04-08T10:35:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/cd/05/9fccc43c50c39a76b68343484b9da7b12d42d0859c37c61aec018c967a32/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a", size = 499741, upload_time = "2025-04-08T10:35:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/499e90c37fa518976782b10a18b18db9f55ea73ca14641615056f8194bb3/watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827", size = 453995, upload_time = "2025-04-08T10:35:46.336Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/f75d6840059320df5adecd2c687fbc18960a7f97b55c300d20f207d48aef/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a", size = 629693, upload_time = "2025-04-08T10:35:48.161Z" }, + { url = "https://files.pythonhosted.org/packages/fc/17/180ca383f5061b61406477218c55d66ec118e6c0c51f02d8142895fcf0a9/watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936", size = 624677, upload_time = "2025-04-08T10:35:49.65Z" }, + { url = "https://files.pythonhosted.org/packages/bf/15/714d6ef307f803f236d69ee9d421763707899d6298d9f3183e55e366d9af/watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc", size = 277804, upload_time = "2025-04-08T10:35:51.093Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087, upload_time = "2025-04-08T10:35:52.458Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload_time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload_time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload_time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload_time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload_time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload_time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload_time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload_time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload_time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload_time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload_time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload_time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload_time = "2025-03-05T20:03:39.41Z" }, +] diff --git a/kontor-spring/.factorypath b/kontor-spring/.factorypath new file mode 100644 index 0000000..5751075 --- /dev/null +++ b/kontor-spring/.factorypath @@ -0,0 +1,3 @@ + + + diff --git a/kontor-spring/.gitattributes b/kontor-spring/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/kontor-spring/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/kontor-spring/.gitignore b/kontor-spring/.gitignore new file mode 100644 index 0000000..9896012 --- /dev/null +++ b/kontor-spring/.gitignore @@ -0,0 +1,33 @@ +.gradle/ +.settings/ +build/ +bin/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +.project +.classpath +.vscode/ +.idea/ +*.lock +logs/ +frontend/generated +frontend/index.html +package*.json +tsconfig.json +types.d.ts +node_modules/ +vite.* +kontor*Db +tags* +kontorHSQLDB* +.vs/ +.winget +src/main/resources/application-local.properties +src/main/resources/application-prod.properties +src/main/resources/application-*.yml +/uploaded-files/ diff --git a/kontor-spring/Dockerfile b/kontor-spring/Dockerfile new file mode 100644 index 0000000..d4f3463 --- /dev/null +++ b/kontor-spring/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine/java:21-jdk +WORKDIR / +ADD build/libs/kontor-spring-0.1.0-SNAPSHOT.jar app.jar +EXPOSE 8000 +CMD ["java", "-jar", "-Dspring.profiles.active=prod", "-Dvaadin.productionMode=true", "app.jar"] diff --git a/kontor-spring/Makefile b/kontor-spring/Makefile new file mode 100644 index 0000000..55728ed --- /dev/null +++ b/kontor-spring/Makefile @@ -0,0 +1,8 @@ +.PYHONY: all + +all: + ./gradlew build + +docker: + ./gradlew dockerImage + diff --git a/kontor-spring/README.md b/kontor-spring/README.md new file mode 100644 index 0000000..f24bbcd --- /dev/null +++ b/kontor-spring/README.md @@ -0,0 +1,3 @@ +# kontor-spring + +Kontor Anwendung mit Spring Boot und Vaadin \ No newline at end of file diff --git a/kontor-spring/build.gradle b/kontor-spring/build.gradle new file mode 100644 index 0000000..0389c14 --- /dev/null +++ b/kontor-spring/build.gradle @@ -0,0 +1,250 @@ +buildscript { + configurations.classpath { + resolutionStrategy.eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'com.burgstaller' && details.requested.name == 'okhttp-digest' && details.requested.version == '1.10') { + details.useTarget "io.github.rburgst:${details.requested.name}:1.21" + details.because 'Dependency has moved' + } + } + } + repositories { + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + } +} + +plugins { + id 'java' + id 'application' + id 'maven-publish' + id "com.google.cloud.artifactregistry.gradle-plugin" version "2.2.0" + id 'jvm-test-suite' + id 'jacoco' + id 'test-report-aggregation' + id 'jacoco-report-aggregation' + alias(libs.plugins.spring.boot) + alias(libs.plugins.spring.dependencies) + alias(libs.plugins.vaadin) + alias(libs.plugins.lombok) + alias(libs.plugins.asciidoctorPdf) + alias(libs.plugins.asciidoctorConvert) + alias(libs.plugins.asciidoctorGems) +} + +repositories { + mavenCentral() + ruby.gems() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { setUrl("https://maven.vaadin.com/vaadin-addons") } +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 +} + +configurations { + developmentOnly + runtimeClasspath { + extendsFrom developmentOnly + } +} + +dependencies { + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.security:spring-security-oauth2-jose' + implementation 'org.springframework.security:spring-security-oauth2-resource-server' + implementation 'com.h2database:h2' + implementation libs.hsqldb + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation libs.mail + implementation libs.jackson + implementation libs.gson + implementation libs.json + implementation 'org.hibernate.orm:hibernate-community-dialects' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'com.vaadin:vaadin-testbench-junit5' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + asciidoctorGems libs.rouge + //asciidoctorGems libs.diagram +} + +def pdfFile = layout.buildDirectory.file("docs/asciidocPdf/kontor-spring.pdf") +def pdfArtifact = artifacts.add('archives', pdfFile.get().asFile) { + type 'pdf' + builtBy asciidoctorPdf +} + +publishing { + publications { + maven(MavenPublication) { + groupId = group + '.docs' + artifactId = project.name + artifact pdfArtifact + } + bootJava(MavenPublication) { + artifact tasks.named("bootDistTar") + } + } + repositories { + maven { + name = "gitlabPackageRegistry" + url = uri("https://gitlab.com/api/v4/projects/64726715/packages/maven") + credentials(PasswordCredentials) + } + } +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +asciidoctorPdf { + dependsOn asciidoctorGemsPrepare + + baseDirFollowsSourceFile() + + asciidoctorj { + modules { + diagram.use() + } + requires 'rouge' + attributes 'build-gradle': file('build.gradle'), + 'endpoint-url': 'https://www.thpeetz.de', + 'source-highlighter': 'rouge', + 'imagesdir': './images', + 'toc': 'left', + 'toc-title': 'Inhaltsverzeichnis', + 'revdate': BUILD_DATE, + 'revnumber': { project.version.toString() }, + 'revremark': 'Entwurf', + 'chapter-label': '', + 'icons': 'font', + 'idprefix': 'id_', + 'idseparator': '-', + 'docinfo1': '' + } +} + +build.dependsOn asciidoctorPdf + +dependencyManagement { + imports { + mavenBom libs.vaadin.bom.get().toString() + } +} + +application { + mainClass = 'de.thpeetz.kontor.Application' +} + +bootRun { + args = ["--spring.profiles.active=${project.properties['profile'] ?: 'prod'}"] +} + +task dockerImage(type: Exec) { + dependsOn(bootJar) + commandLine "docker", "build", ".", "-t", "kontor:${project.version}", "-t", "kontor" +} + +vaadin { + productionMode = true +} + +testing { + suites { + configureEach { + useJUnitJupiter() + dependencies { + implementation project() + implementation 'com.vaadin:vaadin-core' + implementation 'com.vaadin:vaadin-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'com.h2database:h2' + implementation libs.hsqldb + implementation libs.sqlite.jdbc + //runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + implementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + implementation 'org.springframework.security:spring-security-test' + implementation 'com.vaadin:vaadin-testbench-junit5' + implementation 'io.projectreactor:reactor-test' + runtimeOnly 'org.junit.platform:junit-platform-launcher' + } + } + test(JvmTestSuite) { + testType = TestSuiteType.UNIT_TEST + targets { + all { + testTask.configure { + reports { + junitXml { + outputPerTestCase = true // defaults to false + mergeReruns = true // defaults to false + } + } + finalizedBy(jacocoTestReport) + } + } + } + } + integrationTest(JvmTestSuite) { + testType = "view-test" + targets { + all { + testTask.configure { + shouldRunAfter(test) + finalizedBy(jacocoTestReport) + } + } + } + } + } +} + +tasks.named('check') { + dependsOn(testing.suites.integrationTest) + dependsOn(testing.suites.test) + dependsOn tasks.named('testAggregateTestReport', TestReport) + dependsOn tasks.named('integrationTestAggregateTestReport', TestReport) +} + + +jacocoTestReport { + dependsOn test, integrationTest + reports { + xml.required = true + csv.required = false + } +} + +reporting { + reports { + testAggregateTestReport(AggregateTestReport) { + testType = TestSuiteType.UNIT_TEST + } + integrationTestAggregateTestReport(AggregateTestReport) { + testType = "view-test" + } + integrationTestCodeCoverageReport(JacocoCoverageReport) { + testType = "view-test" + } + } +} + +wrapper { + gradleVersion = "8.6" +} diff --git a/kontor-spring/frontend/themes/kontor/styles.css b/kontor-spring/frontend/themes/kontor/styles.css new file mode 100644 index 0000000..843559f --- /dev/null +++ b/kontor-spring/frontend/themes/kontor/styles.css @@ -0,0 +1,10 @@ +@media all and (max-width: 1100px) { + .list-view.editing .toolbar, + .list-view.editing .contact-grid { + display: none; + } +} +a[highlight] { + font-weight: bold; + text-decoration: underline; +} diff --git a/kontor-spring/frontend/themes/kontor/theme.json b/kontor-spring/frontend/themes/kontor/theme.json new file mode 100644 index 0000000..0f7a81f --- /dev/null +++ b/kontor-spring/frontend/themes/kontor/theme.json @@ -0,0 +1,3 @@ +{ + "lumoImports" : [ "typography", "color", "spacing", "badge", "utility" ] +} \ No newline at end of file diff --git a/kontor-spring/gradle.properties b/kontor-spring/gradle.properties new file mode 100644 index 0000000..f305204 --- /dev/null +++ b/kontor-spring/gradle.properties @@ -0,0 +1,3 @@ +description='Kontor with Spring Boot' +version=0.1.0-SNAPSHOT +group=de.thpeetz diff --git a/kontor-spring/gradle/libs.versions.toml b/kontor-spring/gradle/libs.versions.toml new file mode 100644 index 0000000..8f74e77 --- /dev/null +++ b/kontor-spring/gradle/libs.versions.toml @@ -0,0 +1,66 @@ +[versions] +gradle = "8.6" +args4j = "2.33" +commonscli = "1.5.0" +junit = "5.8.2" +logback = "1.1.2" +mockito = "1.9.5" +picoli = "4.7.0" +slf4j = "1.7.22" +hsqldb = "2.7.1" +sqlite = "3.25.2" +spotbugs = "6.0.7" +asciidoctor = "4.0.2" +rouge = "3.15.0" +#diagram = "2.2.2" +diagram = "2.3.1" +sonarqube = "3.3" +cimtConventions = "1.0.0-SNAPSHOT" +springboot = "3.2.5" +springdependencies = "1.1.4" +vaadin = "24.3.8" +lombok = "8.6" +gson = "2.9.0" +jackson = "2.16.1" +json_simple = "1.1.1" +mail = "1.6.2" + +[libraries] +args4j = { module = "args4j:args4j", version.ref = "args4j" } +commonscli = { module = "commons-cli:commons-cli", version.ref = "commonscli" } +junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" } +logbackCore = { module = "ch.qos.logback:logback-core", version.ref = "logback" } +logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +mockito = { module = "org.mockito:mockito-all", version.ref = "mockito" } +picocli = { module = "info.picocli:picocli", version.ref = "picoli" } +slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +hsqldb = { module = "org.hsqldb:hsqldb", version.ref = "hsqldb" } +gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +json = { module = "com.googlecode.json-simple:json-simple", version.ref ="json_simple" } +mail = { module = "com.sun.mail:javax.mail", version.ref ="mail" } +sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" } +vaadin-bom = { module = "com.vaadin:vaadin-bom", version.ref = "vaadin" } +asciidoctorGradleJvmGems = { module = "org.asciidoctor:asciidoctor-gradle-jvm-gems", version.ref= "asciidoctor" } +asciidoctorGradleJvm = { module = "org.asciidoctor:asciidoctor-gradle-jvm", version.ref= "asciidoctor" } +asciidoctorGradleJvmPdf = { module = "org.asciidoctor:asciidoctor-gradle-jvm-pdf", version.ref= "asciidoctor" } +rouge = { module = "rubygems:rouge", version.ref = "rouge" } +diagram = { module = "rubygems:asciidoctor-diagram", version.ref = "diagram" } + +[bundles] +logback = ["logbackCore", "logbackClassic"] + +[plugins] +spotbugs = { id = "com.github.spotbugs", version.ref = "spotbugs" } +sonarqube = { id = "org.sonarqube", version.ref = "sonarqube" } +asciidoctorPdf = { id = "org.asciidoctor.jvm.pdf", version.ref = "asciidoctor" } +asciidoctorConvert = { id = "org.asciidoctor.jvm.convert", version.ref = "asciidoctor" } +asciidoctorGems = { id = "org.asciidoctor.jvm.gems", version.ref = "asciidoctor" } +javaConvention = { id = "de.cimt.java-conventions", version.ref = "cimtConventions" } +applicationConvention = { id = "de.cimt.application-conventions", version.ref = "cimtConventions" } +libraryConvention = { id = "de.cimt.library-conventions", version.ref = "cimtConventions" } +asciidoctorConvention = { id = "de.cimt.asciidoctor-conventions", version.ref = "cimtConventions" } +spring-boot = { id = "org.springframework.boot", version.ref = "springboot"} +spring-dependencies = { id = "io.spring.dependency-management", version.ref = "springdependencies" } +vaadin = { id = "com.vaadin", version.ref = "vaadin" } +lombok = { id = "io.freefair.lombok", version.ref = "lombok" } diff --git a/kontor-spring/gradle/wrapper/gradle-wrapper.jar b/kontor-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/kontor-spring/gradle/wrapper/gradle-wrapper.properties b/kontor-spring/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a80b22c --- /dev/null +++ b/kontor-spring/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kontor-spring/gradlew b/kontor-spring/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/kontor-spring/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kontor-spring/gradlew.bat b/kontor-spring/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/kontor-spring/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kontor-spring/settings.gradle b/kontor-spring/settings.gradle new file mode 100644 index 0000000..f559dac --- /dev/null +++ b/kontor-spring/settings.gradle @@ -0,0 +1,24 @@ +pluginManagement { + resolutionStrategy { + eachPlugin { + if (requested.id.id == 'org.springframework.boot') { + useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") + } + if (requested.id.id == 'org.gradle.toolchains.foojay-resolver') { + useModule("org.gradle.toolchains.foojay-resolver-convention:0.4.0") + } + } + } + repositories { + gradlePluginPortal() + mavenCentral() + maven { setUrl("https://maven.vaadin.com/vaadin-prereleases") } + maven { setUrl("https://repo.spring.io/milestone") } + maven { url 'https://plugins.gradle.org/m2/' } + } +// plugins { +// id 'com.vaadin' version "${vaadinVersion}" +// } +} + +rootProject.name = 'kontor-spring' diff --git a/kontor-spring/src/docs/asciidoc/kontor-spring.adoc b/kontor-spring/src/docs/asciidoc/kontor-spring.adoc new file mode 100644 index 0000000..bd4ee00 --- /dev/null +++ b/kontor-spring/src/docs/asciidoc/kontor-spring.adoc @@ -0,0 +1,509 @@ += Projektbeschreibung kontor-spring: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-spring und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitea + +Für die Verwaltung des Sourcecode kommt ((Gitea))<> zum Einsatz. +Mit Gitea werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitea.thpeetz.de/kontor/kontor-spring + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +===== Benutzer ER-Diagramm + +[mermaid, kontor-user-er, png] +.Benutzer ER-Diagramm +.... +erDiagram + user { + string id PK + datetime created_date + datetime last_modified_date + int version + string email + boolean enabled + string firstName + string lastName + string password + string token + boolean tokenExpired + string userName UNIQUE + } + role { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + authorization_matrix { + string id PK + datetime created_date + datetime last_modified_date + int version + string user_id FK + string role_id FK + } + module_data { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean import_data + string module_name UNIQUE + } + user ||--o{ authorization_matrix : "matrix" + role ||--o{ authorization_matrix : "matrix" +.... + + +===== Comics ER-Diagramm + +[mermaid, kontor-comics-er, png] +.Comics ER-Diagramm +.... +erDiagram + comic { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean completed + boolean currentOrder + string title + string publisher_id FK + } + volume { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + issue { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean in_stock + boolean is_read + string issue_number + string comic_id FK + string volume_id FK + } + publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + artist { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + story_arc { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string comic_id FK + } + trade_paperback { + string id PK + datetime created_date + datetime last_modified_date + int version + int issueStart + int issueEnd + string name + string comic_id FK + } + worktype { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + comic_work { + string id PK + datetime created_date + datetime last_modified_date + int version + string artist_id FK + string comic_id FK + string worktype_id FK + } + comic ||--o{ comic_work : "1" + artist ||--o{ comic_work : "1" + worktype ||--o{ comic-work : "1" + publisher ||--o{ comic : "1" + comic ||--o{ issue : "1" + comic ||--o{ volume : "1" + comic ||--o{ story_arc : "1" + comic ||--o{ trade_paperback : "1" + volume ||--o{ issue : "1" +.... + +===== TYSC ER-Diagramm + +[mermaid, kontor-tysc-er, png] +.TYSC ER-Diagramm +.... +erDiagram + sport { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + team { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + field_position { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + string short_name + string sport_id FK + } + rooster { + string id PK + datetime created_date + datetime last_modified_date + int version + int year + string player_id FK + string position_id FK + string team_id FK + } + player { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + vendor { + string id PK + datetime created_date + datetime last_modified_date + int version + string name + } + card_set { + string id PK + datetime created_date + datetime last_modified_date + int version + boolean insert_set + string name + boolean parallel_set + string vendor_id FK + } + card { + string id PK + datetime created_date + datetime last_modified_date + int version + int cardNumber + int year + string card_set FK + string rooster_id FK + string vendor_id FK + } + sport ||--o{ team : "1" + sport ||--o{ field_position : "1" + field_position ||--o{ rooster : "1" + player ||--o{ rooster : "1" + team ||--o{ rooster : "1" + vendor ||--o{ card : "1" + card_set ||--o{ card : "1" + rooster ||--o{ card : "1" +.... + +===== Bookshelf ER-Diagramm + +[mermaid, kontor-bookshelf-er, png] +.Bookshelf ER-Diagramm +.... +erDiagram + article { + string id PK + datetime created_date + datetime last_modified_date + int version + string title + } + book { + string id PK + datetime created_date + datetime last_modified_date + int version + string isbn UNIQUE + string title + int year + string publisher_id FK + } + bookshelf_publisher { + string id PK + datetime created_date + datetime last_modified_date + int version + string name UNIQUE + } + author { + string id PK + datetime created_date + datetime last_modified_date + int version + string first_name + string last_name + } + article_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string article_id FK + string author_id FK + } + book_author { + string id PK + datetime created_date + datetime last_modified_date + int version + string book_id FK + string author_id FK + } + publisher ||--o{ book : "1" + article ||--o{ article_author : "1" + author ||--o{ article_author : "1" + book ||--o{ book_author : "1" + author ||--o{ book_author : "1" +.... + +===== Mail ER-Diagramm + +[mermaid, kontor-mail-er, png] +.Mail ER-Diagramm +.... +erDiagram + mail { + string id PK + datetime created_date + datetime last_modified_date + int version + string subject + string content + datetime received_date + datetime sent_date + } + mail_account { + string id PK + datetime created_date + datetime last_modified_date + int version + string host + string password + int port + string protocol + boolean start_tls + string user_name + } + mail_address { + string id PK + datetime created_date + datetime last_modified_date + int version + string internet_address UNIQUE + string personal + string user_id FK + } + user ||--o{ mail_address : "1" +.... + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitea]]] http://www.gitea.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java new file mode 100644 index 0000000..e2eec07 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistViewTest { + + @Autowired + private ArtistView artistView; + + @Test + void formShownWhenArtistSelected() { + Grid grid = artistView.getGrid(); + Artist firstArtist = getFirstItem(grid); + + ArtistForm form = artistView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstArtist); + assertTrue(form.isVisible()); + assertEquals(firstArtist.getName(), form.name.getValue()); + } + + private Artist getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List artists = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(5, count); + return artists.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java new file mode 100644 index 0000000..a6aea11 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ArtistformTest.java @@ -0,0 +1,63 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.data.Artist; + +@SpringBootTest +class ArtistformTest { + + private Artist artist1; + private static final String ARTISTNAME= "Lee, Stan"; + + @BeforeEach + void setupData() { + artist1 = new Artist(); + artist1.setName(ARTISTNAME); + } + + @Test + void formFieldsPopulated() { + ArtistForm form = new ArtistForm(); + form.setArtist(artist1); + assertEquals(ARTISTNAME, form.name.getValue()); + } + + @Test + void saveEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference savedArtistReference = new AtomicReference<>(null); + form.addSaveListener(e -> { + savedArtistReference.set(e.getArtist()); + }); + form.save.click(); + Artist savedArtist = savedArtistReference.get(); + assertEquals(ARTISTNAME, savedArtist.getName()); + } + + @Test + void deleteEventHasCorrectValues() { + ArtistForm form = new ArtistForm(); + Artist artist = new Artist(); + form.setArtist(artist); + form.name.setValue(ARTISTNAME); + + AtomicReference deletedArtistReference = new AtomicReference<>(null); + form.addDeleteListener(e -> { + deletedArtistReference.set(e.getArtist()); + }); + form.delete.click(); + Artist deletedArtist = deletedArtistReference.get(); + assertEquals(ARTISTNAME, deletedArtist.getName()); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java new file mode 100644 index 0000000..084d64e --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Comic; + +@SpringBootTest +public class ComicViewTest { + + @Autowired + private ComicView comicView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicView.getGrid(); + Comic firstComic = getFirstItem(grid); + + ComicForm form = comicView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComic); + assertTrue(form.isVisible()); + assertEquals(firstComic.getTitle(), form.title.getValue()); + } + + private Comic getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comics = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(169, count); + return comics.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java new file mode 100644 index 0000000..0d3113e --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/ComicWorkViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.ComicWork; + +@SpringBootTest +class ComicWorkViewTest { + + @Autowired + private ComicWorkView comicWorkView; + + @Test + void formShownWhenComicSelected() { + Grid grid = comicWorkView.getGrid(); + ComicWork firstComicWork = getFirstItem(grid); + + ComicWorkForm form = comicWorkView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstComicWork); + assertTrue(form.isVisible()); + assertEquals(firstComicWork.getComic(), form.comic.getValue()); + } + + private ComicWork getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List comicWorks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return comicWorks.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java new file mode 100644 index 0000000..089f8f5 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/IssueViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Issue; + +@SpringBootTest +public class IssueViewTest { + + @Autowired + private IssueView issueView; + + @Test + void formShownWhenIssueSelected() { + Grid grid = issueView.getGrid(); + Issue firstIssue = getFirstItem(grid); + + IssueForm form = issueView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstIssue); + assertTrue(form.isVisible()); + assertEquals(firstIssue.getIssueNumber(), form.issueNumber.getValue()); + } + + private Issue getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List issues = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(750, count); + return issues.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java new file mode 100644 index 0000000..b3bab80 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/PublisherViewTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Publisher; + +@SpringBootTest +class PublisherViewTest { + + @Autowired + private PublisherView publisherView; + + @Test + void formShownWhenPublisherSelected() { + Grid grid = publisherView.getGrid(); + Publisher firstPublisher = getFirstItem(grid); + + PublisherForm form = publisherView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPublisher); + assertTrue(form.isVisible()); + assertEquals(firstPublisher.getName(), form.name.getValue()); + } + + private Publisher getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List publishers = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(18, count); + return publishers.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java new file mode 100644 index 0000000..56f9c34 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/StoryArcViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.StoryArc; + +@SpringBootTest +class StoryArcViewTest { + + @Autowired + private StoryArcView storyArcView; + + @Test + void formShownWhenStoryArcSelected() { + Grid grid = storyArcView.getGrid(); + StoryArc firstStoryArc = getFirstItem(grid); + + StoryArcForm form = storyArcView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstStoryArc); + assertTrue(form.isVisible()); + assertEquals(firstStoryArc.getName(), form.name.getValue()); + } + + private StoryArc getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List storyArcs = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(3, count); + return storyArcs.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java new file mode 100644 index 0000000..a3b4119 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/TradePaperbackViewTest.java @@ -0,0 +1,47 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class TradePaperbackViewTest { + + @Autowired + private TradePaperbackView tradePaperbackView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = tradePaperbackView.getGrid(); + + TradePaperback firstTradePaperback = getFirstItem(grid); + + TradePaperBackForm form = tradePaperbackView.getForm(); + assertFalse(form.isVisible()); + + if (firstTradePaperback != null) { + grid.asSingleSelect().setValue(firstTradePaperback); + assertTrue(form.isVisible()); + assertEquals(firstTradePaperback.getName(), form.name.getValue()); + } + } + + private TradePaperback getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List tradePaperbacks = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(40, count); + return tradePaperbacks.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java new file mode 100644 index 0000000..15e8d15 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/VolumeViewTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Volume; + +@SpringBootTest +class VolumeViewTest { + + @Autowired + private VolumeView volumeView; + + @Test + void formShownWhenVolumeSelected() { + Grid grid = volumeView.getGrid(); + + Volume firstVolume = getFirstItem(grid); + + VolumeForm form = volumeView.getForm(); + assertFalse(form.isVisible()); + + if (firstVolume != null) { + grid.asSingleSelect().setValue(firstVolume); + assertTrue(form.isVisible()); + assertEquals(firstVolume.getName(), form.name.getValue()); + } + } + + private Volume getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List volumes = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(0, count); + if (count > 0) { + return volumes.get(0); + } else { + return null; + } + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java new file mode 100644 index 0000000..2f15672 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/comics/views/WorktypeViewTest.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.comics.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.comics.data.Worktype; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +class WorktypeViewTest { + + @Autowired + private WorktypeView worktypeView; + + @Test + void formShownWhenWorktypeSelected() { + Grid grid = worktypeView.getGrid(); + + Worktype firstWorktype = getFirstItem(grid); + + WorktypeForm form = worktypeView.getForm(); + assertFalse(form.isVisible()); + + if (firstWorktype != null) { + grid.asSingleSelect().setValue(firstWorktype); + assertTrue(form.isVisible()); + assertEquals(firstWorktype.getName(), form.name.getValue()); + } + } + + private Worktype getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List worktypes = grid.getListDataView().getItems().collect(Collectors.toList()); + log.info("found worktypes: {}", worktypes); + assertEquals(3, count); + return worktypes.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java new file mode 100644 index 0000000..40d3fbb --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardSetViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.CardSet; + +@SpringBootTest +class CardSetViewTest { + + @Autowired + private CardSetView cardSetView; + + @Test + void formShownWhenCardSetSelected() { + Grid grid = cardSetView.getGrid(); + CardSet firstCardSet = getFirstItem(grid); + + CardSetForm form = cardSetView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCardSet); + assertTrue(form.isVisible()); + assertEquals(firstCardSet.getName(), form.name.getValue()); + } + + private CardSet getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cardSets = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(15, count); + return cardSets.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java new file mode 100644 index 0000000..0a5a3ed --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/CardViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Card; + +@SpringBootTest +class CardViewTest { + + @Autowired + private CardView cardView; + + @Test + void formShownWhenCardSelected() { + Grid grid = cardView.getGrid(); + Card firstCard = getFirstItem(grid); + + CardForm form = cardView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstCard); + assertTrue(form.isVisible()); + assertEquals(String.valueOf(firstCard.getCardNumber()), form.cardNumber.getValue()); + } + + private Card getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List cards = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(10, count); + return cards.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java new file mode 100644 index 0000000..737b74d --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/FieldPositionViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.FieldPosition; + +@SpringBootTest +class FieldPositionViewTest { + + @Autowired + private PositionView positionView; + + @Test + void formShownWhenPositionSelected() { + Grid grid = positionView.getGrid(); + FieldPosition firstFieldPosition = getFirstItem(grid); + + PositionForm form = positionView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstFieldPosition); + assertTrue(form.isVisible()); + assertEquals(firstFieldPosition.getName(), form.name.getValue()); + } + + private FieldPosition getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List positions = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(44, count); + return positions.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java new file mode 100644 index 0000000..5e7d640 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/PlayerViewTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Player; + +@SpringBootTest +class PlayerViewTest { + + @Autowired + private PlayerView playerView; + + @Test + void formShownWhenPlayerSelected() { + Grid grid = playerView.getGrid(); + Player firstPlayer = getFirstItem(grid); + + PlayerForm form = playerView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstPlayer); + assertTrue(form.isVisible()); + assertEquals(firstPlayer.getLastName(), form.lastName.getValue()); + assertEquals(firstPlayer.getFirstName(), form.firstName.getValue()); + } + + private Player getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List players = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(38, count); + return players.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java new file mode 100644 index 0000000..d04f306 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/RoosterViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Rooster; + +@SpringBootTest +class RoosterViewTest { + + @Autowired + private RoosterView roosterView; + + @Test + void formShownWhenRoosterSelected() { + Grid grid = roosterView.getGrid(); + Rooster firstRooster = getFirstItem(grid); + + RoosterForm form = roosterView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstRooster); + assertTrue(form.isVisible()); + assertEquals(firstRooster.getYear(), form.year.getValue()); + } + + private Rooster getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List roosters = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(11, count); + return roosters.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java new file mode 100644 index 0000000..ca86d0a --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/SportViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Sport; + +@SpringBootTest +class SportViewTest { + + @Autowired + private SportView sportView; + + @Test + void formShownWhenSportSelected() { + Grid grid = sportView.getGrid(); + Sport firstSport = getFirstItem(grid); + + SportForm form = sportView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstSport); + assertTrue(form.isVisible()); + assertEquals(firstSport.getName(), form.name.getValue()); + } + + private Sport getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List sports = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(4, count); + return sports.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java new file mode 100644 index 0000000..da0ea19 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/TeamViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +class TeamViewTest { + + @Autowired + private TeamView teamView; + + @Test + void formShownWhenTeamSelected() { + Grid grid = teamView.getGrid(); + Team firstTeam = getFirstItem(grid); + + TeamForm form = teamView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstTeam); + assertTrue(form.isVisible()); + assertEquals(firstTeam.getName(), form.name.getValue()); + } + + private Team getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List teams = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(122, count); + return teams.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java new file mode 100644 index 0000000..ef80d22 --- /dev/null +++ b/kontor-spring/src/integrationTest/java/de/thpeetz/kontor/tysc/views/VendorViewTest.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.tysc.views; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.vaadin.flow.component.grid.Grid; + +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +class VendorViewTest { + + @Autowired + private VendorView vendorView; + + @Test + void formShownWhenVendorSelected() { + Grid grid = vendorView.getGrid(); + Vendor firstVendor = getFirstItem(grid); + + VendorForm form = vendorView.getForm(); + + assertFalse(form.isVisible()); + grid.asSingleSelect().setValue(firstVendor); + assertTrue(form.isVisible()); + assertEquals(firstVendor.getName(), form.name.getValue()); + } + + private Vendor getFirstItem(Grid grid) { + int count = grid.getListDataView().getItemCount(); + List vendors = grid.getListDataView().getItems().collect(Collectors.toList()); + assertEquals(9, count); + return vendors.get(0); + } +} diff --git a/kontor-spring/src/integrationTest/resources/application.properties b/kontor-spring/src/integrationTest/resources/application.properties new file mode 100644 index 0000000..27a1935 --- /dev/null +++ b/kontor-spring/src/integrationTest/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:itDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorITDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/Application.java b/kontor-spring/src/main/java/de/thpeetz/kontor/Application.java new file mode 100644 index 0000000..c304f35 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/Application.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import com.vaadin.flow.component.page.AppShellConfigurator; +import com.vaadin.flow.server.PWA; +import com.vaadin.flow.theme.Theme; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Slf4j +@EnableJpaAuditing +@SpringBootApplication +@Theme(value = "kontor") +@PWA(name = "Vaadin Kontor", shortName = "Kontor", offlinePath = "offline.html", offlineResources = { "images/offline.png" }) +public class Application implements AppShellConfigurator { + + public static void main(String[] args) { + log.info("Starting Kontor application"); + SpringApplication.run(Application.class); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java new file mode 100644 index 0000000..a3ef2ff --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/AdminConstants.java @@ -0,0 +1,56 @@ +package de.thpeetz.kontor.admin; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import de.thpeetz.kontor.admin.views.*; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.views.ComicWorkView; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.views.MediaActorFileView; + +public class AdminConstants { + + + private AdminConstants() { + // private constructor to hide the implicit public one + } + + public static final String ADMIN_TITLE = "Verwaltung"; + public static final String AUTHORIZATION = "Berechtigungen"; + public static final String ASSIGNMENT_ROUTE = "/admin/assignment"; + public static final String DATA = "Daten"; + public static final String METADATA_ROUTE = "admin/metadata"; + public static final String PERMISSION = "Berechtigungen"; + public static final String PERMISSION_ROUTE = "/admin/permission"; + public static final String PROFILE = "Benutzer"; + public static final String PROFILE_ROUTE = "/admin/profile"; + public static final String ADMIN = "admin"; + public static final String ADMIN_ROUTE = "/admin"; + + public static RouterLink getProfileNavigation() { + return new RouterLink(PROFILE, ProfileView.class); + } + + public static RouterLink getPermissionNavigation() { + return new RouterLink(PERMISSION, PermissionView.class); + } + + public static RouterLink getAuthorizationNavigation() { + return new RouterLink(AUTHORIZATION, AssignmentView.class); + } + + public static SideNavItem getAdminNavigation() { + SideNavItem administration = new SideNavItem(ADMIN_TITLE, PROFILE_ROUTE, VaadinIcon.GROUP.create()); + administration.addItem(new SideNavItem(PROFILE, PROFILE_ROUTE, VaadinIcon.USERS.create())); + administration.addItem(new SideNavItem(PERMISSION, PermissionView.class)); + SideNavItem data = new SideNavItem(DATA, ASSIGNMENT_ROUTE, VaadinIcon.DATABASE.create()); + data.addItem(new SideNavItem(ComicConstants.COMICWORK, ComicWorkView.class)); + data.addItem(new SideNavItem(MediaConstants.MEDIAACTORFILE, MediaActorFileView.class)); + data.addItem(new SideNavItem(AUTHORIZATION, AssignmentView.class)); + data.addItem(new SideNavItem("Data Import", ModuleDataView.class)); + data.addItem(new SideNavItem("Meta Data", MetaDataView.class)); + administration.addItem(data); + return administration; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/MailProperties.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/MailProperties.java new file mode 100644 index 0000000..9e8a6b4 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/MailProperties.java @@ -0,0 +1,78 @@ +package de.thpeetz.kontor.admin; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + + private String protocol; + private String host; + private Integer port; + private String userName; + private String password; + private Boolean startTls; + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public Boolean getStartTls() { + return startTls; + } + + public void setStartTls(Boolean startTls) { + this.startTls = startTls; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("MailProperties{"); + sb.append("protocol='").append(protocol).append('\''); + sb.append(", host='").append(host).append('\''); + sb.append(", port=").append(port); + sb.append(", starttls=").append(startTls); + sb.append(", userName='").append(userName).append('\''); + sb.append(", password='").append(password).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java new file mode 100644 index 0000000..44e28de --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/SetupModuleAdmin.java @@ -0,0 +1,434 @@ +package de.thpeetz.kontor.admin; + +import de.thpeetz.kontor.admin.data.*; +import de.thpeetz.kontor.admin.repository.*; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.mailclient.data.MailAccount; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.List; + +@Slf4j +@Component +public class SetupModuleAdmin implements ApplicationListener { + boolean alreadySetup = false; + + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private AssignmentRepository assignmentRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private MailAccountRepository mailAccountRepository; + + @Autowired + private MailProperties mailProperties; + + @Autowired + private AdminService adminService; + + @Autowired + private MetaDataService metaDataService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + return; + } + + // Create initial roles and users + Permission adminPermission = adminService.addPermission("ROLE_ADMIN"); + Permission userPermission = adminService.addPermission("ROLE_USER"); + List profiles = profileRepository.findAll(); + if (profiles.isEmpty()) { + Profile adminProfile = initAdminUser(); + initAssignments(adminPermission, adminProfile); + initAssignments(userPermission, adminProfile); + } + log.info("MailProperties: {}", mailProperties); + initMail(mailProperties); + + initMetaData(); + } + + private void initMail(MailProperties mailProperties) { + log.info("initMail: Host {} with User {}", mailProperties.getHost(), mailProperties.getUserName()); + if (mailProperties.getHost() == null || mailProperties.getHost().isEmpty()) { + return; + } + boolean addMailAccount = false; + List mailAccounts = mailAccountRepository.findAll(); + if (mailAccounts.isEmpty()) { + addMailAccount = true; + } + for (MailAccount mailAccount : mailAccounts) { + String accountUser = mailAccount.getUserName(); + String propertyUser = mailProperties.getUserName(); + String accountHost = mailAccount.getHost(); + String propertyHost = mailProperties.getHost(); + if (propertyHost.equals(accountHost) && propertyUser.equals(accountUser)) { + log.debug("already configured: {}", mailAccount); + } else { + addMailAccount = true; + } + } + if (addMailAccount) { + log.info("add Mail Account: {}", mailProperties); + MailAccount mailAccount = new MailAccount(); + mailAccount.setProtocol(mailProperties.getProtocol()); + mailAccount.setHost(mailProperties.getHost()); + mailAccount.setPort(mailProperties.getPort()); + mailAccount.setUserName(mailProperties.getUserName()); + mailAccount.setPassword(mailProperties.getPassword()); + mailAccount.setStartTls(mailProperties.getStartTls()); + mailAccountRepository.save(mailAccount); + } + } + + private void initAssignments(Permission permission, Profile profile) { + log.info("initAssignments: Permission {} for Profile {}", permission.getName(), profile.getUserName()); + Collection configuredRoles = assignmentRepository.findByProfile(profile); + if (configuredRoles.stream().anyMatch(assignment -> assignment.getPermission().getId().equals(permission.getId()))) { + log.info("Permission {} already defined", permission.getName()); + } else { + log.info("add Permission {} to Profile {}", permission.getName(), profile.getUserName()); + final Assignment assignment = new Assignment(); + assignment.setProfile(profile); + assignment.setPermission(permission); + assignmentRepository.save(assignment); + } + } + + private Profile initAdminUser() { + log.info("initAdminUser"); + Profile admin = profileRepository.findByUserName("admin"); + if (admin == null) { + log.info("Profile admin not found, will be created."); + admin = new Profile(); + admin.setFirstName("Admin"); + admin.setLastName("Administrator"); + admin.setUserName("admin"); + admin.setEmail("admin@example.org"); + admin.setPassword(passwordEncoder.encode("admin")); + profileRepository.save(admin); + } + return admin; + } + + private void initMetaData() { + log.info("initMetaData"); + MetaDataTable mediaArticleTable = metaDataService.getTable("media_article"); + metaDataService.getColumn(mediaArticleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(mediaArticleTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaArticleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaArticleTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaArticleTable, "url", "link_url", "TEXT", "UNIQUE", 5, Boolean.TRUE, "URL", Boolean.FALSE, null); + metaDataService.getColumn(mediaArticleTable, "review", "review", "BOOLEAN", null, 6, Boolean.TRUE, "Review", Boolean.TRUE, "Review"); + metaDataService.getColumn(mediaArticleTable, "title", "title", "TEXT", null, 7, Boolean.TRUE, "Title", Boolean.FALSE, null); + MetaDataTable mediaVideoTable = metaDataService.getTable("media_video"); + metaDataService.getColumn(mediaVideoTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "Version", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "url", "link_url", "TEXT", "UNIQUE", 5, Boolean.TRUE, "URL", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "review", "review", "BOOLEAN", null, 6, Boolean.TRUE, "Review", Boolean.TRUE, "Review"); + metaDataService.getColumn(mediaVideoTable, "should_download", "should_download", "BOOLEAN", null, 7, Boolean.TRUE, "Download", Boolean.TRUE, "Download"); + metaDataService.getColumn(mediaVideoTable, "title", "title", "TEXT", null, 8, Boolean.TRUE, "Title", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "file_name", "file_name", "TEXT", null, 9, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "path", "path", "TEXT", null, 10, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mediaVideoTable, "cloud_link", "cloud_link", "TEXT", null, 11, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable mediaFileTable = metaDataService.getTable("media_file"); + metaDataService.getColumn(mediaFileTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "Created", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "Modified", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "Version", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "url", "link_url", "TEXT", "UNIQUE", 5, Boolean.TRUE, "URL", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "review", "review", "BOOLEAN", null, 6, Boolean.TRUE, "Review", Boolean.TRUE, "Review"); + metaDataService.getColumn(mediaFileTable, "should_download", "should_download", "BOOLEAN", null, 7, Boolean.TRUE, "Download", Boolean.TRUE, "Download"); + metaDataService.getColumn(mediaFileTable, "title", "title", "TEXT", null, 8, Boolean.TRUE, "Title", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "file_name", "file_name", "TEXT", null, 9, Boolean.TRUE, "Dateiname", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "path", "path", "TEXT", null, 10, Boolean.TRUE, "Verzeichnis", Boolean.FALSE, null); + metaDataService.getColumn(mediaFileTable, "cloud_link", "cloud_link", "TEXT", null, 11, Boolean.TRUE, "Cloud Link", Boolean.FALSE, null); + MetaDataTable mediaActorTable = metaDataService.getTable("media_actor"); + metaDataService.getColumn(mediaActorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "Created", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "Modified", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "Version", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.TRUE, "", Boolean.FALSE, null); + MetaDataTable mediaActorFileTable = metaDataService.getTable("media_actor_file"); + metaDataService.getColumn(mediaActorFileTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorFileTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "Created", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorFileTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "Modified", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorFileTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "Version", Boolean.FALSE, null); + metaDataService.getColumn(mediaActorFileTable, "media_actor_id", "media_actor_id", "TEXT", null, 5, Boolean.TRUE, "Actor", Boolean.FALSE, null, "name"); + metaDataService.getColumn(mediaActorFileTable, "media_file_id", "media_file_id", "TEXT", null, 6, Boolean.TRUE, "File", Boolean.FALSE, null, "title"); + MetaDataTable artistTable = metaDataService.getTable("artist"); + metaDataService.getColumn(artistTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(artistTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(artistTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(artistTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(artistTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable publisherTable = metaDataService.getTable("publisher"); + metaDataService.getColumn(publisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(publisherTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(publisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(publisherTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(publisherTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.TRUE, "", Boolean.FALSE, null); + MetaDataTable comicTable = metaDataService.getTable("comic"); + metaDataService.getColumn(comicTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "ID", Boolean.FALSE, null); + metaDataService.getColumn(comicTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicTable, "completed", "completed", "BOOLEAN", null, 5, Boolean.TRUE, "Complete", Boolean.TRUE, "Complete"); + metaDataService.getColumn(comicTable, "current_order", "current_order", "BOOLEAN", null, 6, Boolean.TRUE, "Bestellung", Boolean.TRUE, "Bestellung"); + metaDataService.getColumn(comicTable, "title", "title", "TEXT", "UNIQUE", 7, Boolean.TRUE, "Title", Boolean.FALSE, null); + metaDataService.getColumn(comicTable, "publisher_id", "publisher_id", "TEXT", null, 8, Boolean.TRUE, "Verlag", Boolean.FALSE, null, "name"); + MetaDataTable issueTable = metaDataService.getTable("issue"); + metaDataService.getColumn(issueTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "in_stock", "in_stock", "BOOLEAN", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "is_read", "is_read", "BOOLEAN", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "issue_number", "issue_number", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(issueTable, "comic_id", "comic_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null, "title"); + metaDataService.getColumn(issueTable, "volume_id", "volume_id", "TEXT", null, 9, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable volumeTable = metaDataService.getTable("volume"); + metaDataService.getColumn(volumeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(volumeTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(volumeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(volumeTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(volumeTable, "name", "name", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(volumeTable, "comic_id", "comic_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable tpbTable = metaDataService.getTable("trade_paperback"); + metaDataService.getColumn(tpbTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "issue_start", "issue_start", "LONG", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "issue_end", "issue_end", "LONG", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "name", "name", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tpbTable, "comic_id", "comic_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null, "title"); + MetaDataTable storyArcTable = metaDataService.getTable("story_arc"); + metaDataService.getColumn(storyArcTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(storyArcTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(storyArcTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(storyArcTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(storyArcTable, "name", "name", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(storyArcTable, "comic_id", "comic_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable worktypeTable = metaDataService.getTable("worktype"); + metaDataService.getColumn(worktypeTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(worktypeTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(worktypeTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(worktypeTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(worktypeTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable comicworkTable = metaDataService.getTable("comic_work"); + metaDataService.getColumn(comicworkTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "artist_id", "artist_id", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "comic_id", "comic_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(comicworkTable, "work_type_id", "work_type_id", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable authorTable = metaDataService.getTable("author"); + metaDataService.getColumn(authorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(authorTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(authorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(authorTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(authorTable, "first_name", "first_name", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(authorTable, "last_name", "last_name", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable articleTable = metaDataService.getTable("article"); + metaDataService.getColumn(articleTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleTable, "title", "title", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable articleAuthorTable = metaDataService.getTable("article_author"); + metaDataService.getColumn(articleAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleAuthorTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleAuthorTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleAuthorTable, "article_id", "article_id", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(articleAuthorTable, "author_id", "author_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable bookTable = metaDataService.getTable("book"); + metaDataService.getColumn(bookTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "isbn", "isbn", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "title", "title", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "year", "year", "LONG", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookTable, "publisher_id", "publisher_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable bookAuthorTable = metaDataService.getTable("book_author"); + metaDataService.getColumn(bookAuthorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookAuthorTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookAuthorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookAuthorTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookAuthorTable, "author_id", "author_id", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookAuthorTable, "book_id", "book_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable bookshelfPublisherTable = metaDataService.getTable("bookshelf_publisher"); + metaDataService.getColumn(bookshelfPublisherTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookshelfPublisherTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookshelfPublisherTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookshelfPublisherTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(bookshelfPublisherTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable sportTable = metaDataService.getTable("sport"); + metaDataService.getColumn(sportTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(sportTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(sportTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(sportTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(sportTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable playerTable = metaDataService.getTable("player"); + metaDataService.getColumn(playerTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(playerTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(playerTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(playerTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(playerTable, "first_name", "first_name", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(playerTable, "last_name", "last_name", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable teamTable = metaDataService.getTable("team"); + metaDataService.getColumn(teamTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "short_name", "short_name", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(teamTable, "sport_id", "sport_id", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable vendorTable = metaDataService.getTable("vendor"); + metaDataService.getColumn(vendorTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(vendorTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(vendorTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(vendorTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(vendorTable, "name", "name", "TEXT", "UNIQUE", 5, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable fieldPositionTable = metaDataService.getTable("field_position"); + metaDataService.getColumn(fieldPositionTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "name", "name", "TEXT", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "short_name", "short_name", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(fieldPositionTable, "sport_id", "sport_id", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable roosterTable = metaDataService.getTable("rooster"); + metaDataService.getColumn(roosterTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "year", "year", "LONG", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "player_id", "player_id", "TEXT", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "position_id", "position_id", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(roosterTable, "team_id", "team_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable cardSetTable = metaDataService.getTable("card_set"); + metaDataService.getColumn(cardSetTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "insert_set", "insert_set", "BOOLEAN", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "parallel_set", "parallel_set", "BOOLEAN", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "name", "name", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardSetTable, "vendor_id", "vendor_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable cardTable = metaDataService.getTable("card"); + metaDataService.getColumn(cardTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "card_number", "card_number", "LONG", null, 5, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "year", "year", "LONG", null, 6, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "card_set_id", "card_set_id", "TEXT", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "rooster_id", "rooster_id", "TEXT", null, 8, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(cardTable, "vendor_id", "vendor_id", "TEXT", null, 9, Boolean.FALSE, "", Boolean.FALSE, null); + MetaDataTable profileTable = metaDataService.getTable("profile"); + metaDataService.getColumn(profileTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "first_name", "first_name", "TEXT", null, 5, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "last_name", "last_name", "TEXT", null, 6, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "user_name", "user_name", "TEXT", "UNIQUE", 7, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "email", "email", "TEXT", null, 8, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "password", "password", "TEXT", null, 9, Boolean.FALSE, "Password", Boolean.FALSE, null); + metaDataService.getColumn(profileTable, "enabled", "enabled", "BOOLEAN", null, 10, Boolean.TRUE, "", Boolean.TRUE, null); + MetaDataTable tokenTable = metaDataService.getTable("token"); + metaDataService.getColumn(tokenTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "token", "token", "TEXT", null, 5, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "name", "name", "TEXT", null, 6, Boolean.TRUE, "Name", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "last_used_date", "used", "TIMESTAMP", null, 7, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(tokenTable, "enabled", "enabled", "BOOLEAN", null, 8, Boolean.TRUE, "", Boolean.TRUE, "Enabled"); + metaDataService.getColumn(tokenTable, "profile_id", "profile_id", "TEXT", null, 9, Boolean.TRUE, "Profile", Boolean.FALSE, null, "user_name"); + MetaDataTable permissionTable = metaDataService.getTable("permission"); + metaDataService.getColumn(permissionTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(permissionTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(permissionTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(permissionTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(permissionTable, "name", "name", "TEXT", null, 5, Boolean.TRUE, "", Boolean.FALSE, null); + MetaDataTable AssignmentTable = metaDataService.getTable("assignment"); + metaDataService.getColumn(AssignmentTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(AssignmentTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(AssignmentTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(AssignmentTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(AssignmentTable, "profile_id", "profile_id", "TEXT", null, 5, Boolean.TRUE, "Profile", Boolean.FALSE, null, "user_name"); + metaDataService.getColumn(AssignmentTable, "permission_id", "permission_id", "TEXT", null, 6, Boolean.TRUE, "Permission", Boolean.FALSE, null, "name"); + MetaDataTable moduleDataTable = metaDataService.getTable("module_data"); + metaDataService.getColumn(moduleDataTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(moduleDataTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(moduleDataTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(moduleDataTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(moduleDataTable, "module_name", "module_name", "TEXT", "UNIQUE", 5, Boolean.TRUE, "Module", Boolean.FALSE, null); + metaDataService.getColumn(moduleDataTable, "import_data", "import_data", "BOOLEAN", null, 6, Boolean.TRUE, "Import Data?", Boolean.TRUE, "Import Data"); + MetaDataTable mailAccountTable = metaDataService.getTable("mail_account"); + metaDataService.getColumn(mailAccountTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "host", "host", "TEXT", null, 5, Boolean.TRUE, "Host", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "port", "port", "LONG", null, 6, Boolean.TRUE, "Port", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "protocol", "protocol", "TEXT", null, 7, Boolean.TRUE, "Protocol", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "user_name", "user_name", "TEXT", null, 8, Boolean.TRUE, "Username", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "password", "password", "TEXT", null, 9, Boolean.FALSE, "Password", Boolean.FALSE, null); + metaDataService.getColumn(mailAccountTable, "start_tls", "start_tls", "BOOLEAN", null, 10, Boolean.TRUE, "StartTLS", Boolean.TRUE, "StartTLS"); + MetaDataTable mailTable = metaDataService.getTable("mail"); + metaDataService.getColumn(mailTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "folder", "folder", "TEXT", null, 5, Boolean.TRUE, "Folder", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "subject", "subject", "TEXT", null, 6, Boolean.TRUE, "Subject", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "body", "body", "TEXT", null, 7, Boolean.FALSE, "Body", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "sent_date", "created", "TIMESTAMP", null, 8, Boolean.FALSE, "Gesendet", Boolean.FALSE, null); + metaDataService.getColumn(mailTable, "received_date", "created", "TIMESTAMP", null, 9, Boolean.FALSE, "Empfangen", Boolean.FALSE, null); + MetaDataTable metaDataTableTable = metaDataService.getTable("meta_data_table"); + metaDataService.getColumn(metaDataTableTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.TRUE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataTableTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataTableTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataTableTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataTableTable, "table_name", "table_name", "TEXT", "UNIQUE", 5, Boolean.TRUE, "Table", Boolean.FALSE, null); + MetaDataTable metaDataColumnTable = metaDataService.getTable("meta_data_column"); + metaDataService.getColumn(metaDataColumnTable, "id", "identifier", "TEXT", "PRIMARY KEY", 1, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "created_date", "created", "TIMESTAMP", null, 2, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "last_modified_date", "modified", "TIMESTAMP", null, 3, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "version", "version", "LONG", null, 4, Boolean.FALSE, "", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "column_name", "column_name", "TEXT", null, 5, Boolean.TRUE, "Column", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "column_sync_name", "column_sync_name", "TEXT", null, 6, Boolean.TRUE, "SQLite Column", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "column_type", "column_type", "TEXT", null, 7, Boolean.TRUE, "Type", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "column_modifier", "column_modifier", "TEXT", null, 8, Boolean.TRUE, "Modifier", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "column_order", "column_order", "LONG", null, 9, Boolean.TRUE, "Order", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "is_shown", "is_shown", "BOOLEAN", null, 10, Boolean.TRUE, "Is Shown", Boolean.TRUE, "Is Shown"); + metaDataService.getColumn(metaDataColumnTable, "column_label", "column_label", "TEXT", null, 11, Boolean.TRUE, "Label", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "show_filter", "show_filter", "BOOLEAN", null, 12, Boolean.TRUE, "Show Filter", Boolean.TRUE, "Show Filter"); + metaDataService.getColumn(metaDataColumnTable, "filter_label", "filter_label", "TEXT", null, 13, Boolean.TRUE, "Filter Label", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "ref_column", "ref_column", "TEXT", null, 14, Boolean.TRUE, "Ref Column", Boolean.FALSE, null); + metaDataService.getColumn(metaDataColumnTable, "table_id", "table_id", "TEXT", null, 15, Boolean.TRUE, "Table", Boolean.FALSE, null, "table_name"); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Assignment.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Assignment.java new file mode 100644 index 0000000..0f1f3ab --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Assignment.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +public class Assignment extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "profile_id") + @NotNull + private Profile profile; + + @ManyToOne + @JoinColumn(name = "permission_id") + @NotNull + private Permission permission; + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Assignment{"); + sb.append("profile=").append(profile.getUserName()); + sb.append(", permission=").append(permission.getName()); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java new file mode 100644 index 0000000..cb4baea --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataColumn.java @@ -0,0 +1,69 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "columnName, table_id"), + uniqueConstraints = @UniqueConstraint(columnNames = {"table_id", "columnOrder"}) +) +public class MetaDataColumn extends AbstractEntity { + + @NotNull + private String columnName; + + private String columnSyncName; + + private String columnType; + + @Nullable + private String columnModifier; + + private Integer columnOrder; + + private Boolean isShown; + + private String columnLabel; + + private Boolean showFilter = Boolean.FALSE; + + private String filterLabel; + + private String refColumn; + + @ManyToOne + @JoinColumn(name = "table_id") + @NotNull + private MetaDataTable table; + + public String getTableName() { + return table.getTableName(); + } + + public String updateColumnName(String value) { + if (!this.getColumnName().equals(value)) { + this.setColumnName(value); + log.info("update columnName"); + return "updated " + this.getId() + " with " + value + "\n"; + } + return "no changes for " + this.getId() + "\n"; + } + + public String updateColumnSyncName(String value) { + if (!this.getColumnSyncName().equals(value)) { + this.setColumnSyncName(value); + log.info("update columnSyncName"); + return "updated " + this.getId() + " with " + value + "\n"; + } + return "no changes for " + this.getId() + "\n"; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java new file mode 100644 index 0000000..5fceace --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/MetaDataTable.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "tableName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"tableName"}) +) +public class MetaDataTable extends AbstractEntity { + + @NotNull + private String tableName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "table") + private List tableColumns = new LinkedList<>(); + + public String updateTableName(String value) { + if (!this.getTableName().equals(value)) { + this.setTableName(value); + log.info("update tableName"); + return "updated " + this.getId() + " with " + value; + } + return "no changes for " + this.getId(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java new file mode 100644 index 0000000..b884425 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/ModuleData.java @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Table( + indexes = @Index(columnList = "moduleName"), + uniqueConstraints = @UniqueConstraint(columnNames = {"moduleName"}) +) +public class ModuleData extends AbstractEntity { + + @NotEmpty + private String moduleName; + + private Boolean importData; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Permission.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Permission.java new file mode 100644 index 0000000..d5d27a0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Permission.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import java.util.List; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +public class Permission extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "permission") + @Nullable + private List assignments; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Profile.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Profile.java new file mode 100644 index 0000000..8773fb5 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Profile.java @@ -0,0 +1,57 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; +import java.util.LinkedList; +import java.util.List; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "userName"), uniqueConstraints = @UniqueConstraint(columnNames = {"userName"})) +public class Profile extends AbstractEntity { + + private String firstName; + + private String lastName; + + @NotEmpty + private String userName; + + private String email; + + private String password; + + private boolean enabled; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "profile") + @Nullable + private List assignments = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "profile") + @Nullable + private List tokens = new LinkedList<>(); + + public String getFullName() { + StringBuilder fullNameBuilder = new StringBuilder(); + if (firstName != null) { + fullNameBuilder.append(firstName); + } + if (lastName != null) { + if (!fullNameBuilder.isEmpty()) { + fullNameBuilder.append(" "); + } + fullNameBuilder.append(lastName); + } + return fullNameBuilder.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Token.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Token.java new file mode 100644 index 0000000..afd4167 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/data/Token.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.admin.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import java.util.Date; + +@Slf4j +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "token"), uniqueConstraints = @UniqueConstraint(columnNames = {"token"})) +public class Token extends AbstractEntity { + + private String token; + + private String name; + + private Date lastUsedDate; + + private boolean enabled; + + @ManyToOne + @JoinColumn(name="profile_id") + @NotNull + private Profile profile; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/AssignmentRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/AssignmentRepository.java new file mode 100644 index 0000000..eb20059 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/AssignmentRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.*; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface AssignmentRepository extends JpaRepository { + + List findByProfile(Profile profile); + + List findByPermission(Permission permission); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MailAccountRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MailAccountRepository.java new file mode 100644 index 0000000..1cd3885 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MailAccountRepository.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import org.springframework.data.jpa.repository.JpaRepository; + + +public interface MailAccountRepository extends JpaRepository { +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataColumnRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataColumnRepository.java new file mode 100644 index 0000000..c0259bf --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataColumnRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataTable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MetaDataColumnRepository extends JpaRepository { + + List findByTable(MetaDataTable table); + + @Query("select m from MetaDataColumn m " + + "where lower(m.columnName) like lower(concat('%', :searchTerm, '%')) or lower(m.columnLabel) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataTableRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataTableRepository.java new file mode 100644 index 0000000..4e31cb4 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/MetaDataTableRepository.java @@ -0,0 +1,9 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.MetaDataTable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MetaDataTableRepository extends JpaRepository { + + MetaDataTable findByTableName(String tableName); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ModuleDataRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ModuleDataRepository.java new file mode 100644 index 0000000..ace45ef --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ModuleDataRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.ModuleData; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ModuleDataRepository extends JpaRepository { + + @Query("select m from ModuleData m where lower(m.moduleName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + ModuleData findByModuleName(String moduleName); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/PermissionRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/PermissionRepository.java new file mode 100644 index 0000000..9e6e863 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/PermissionRepository.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.Permission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface PermissionRepository extends JpaRepository { + + @Query("select p from Permission p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + @Query("select p from Permission p " + + "where lower(p.name) like lower(:name) ") + Permission findByName(@Param("name") String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ProfileRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ProfileRepository.java new file mode 100644 index 0000000..6edf48c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/repository/ProfileRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.admin.repository; + +import de.thpeetz.kontor.admin.data.Profile; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.UUID; + +public interface ProfileRepository extends JpaRepository { + + @Query("select p from Profile p " + + "where lower(p.lastName) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Profile findByUserName(String userName); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java new file mode 100644 index 0000000..0bffa05 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/AdminService.java @@ -0,0 +1,107 @@ +package de.thpeetz.kontor.admin.services; + +import java.util.Collection; +import java.util.List; + +import de.thpeetz.kontor.admin.data.*; +import de.thpeetz.kontor.admin.repository.*; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class AdminService { + + + private final ProfileRepository profileRepository; + private final PermissionRepository permissionRepository; + private final AssignmentRepository assignmentRepository; + + public AdminService(ProfileRepository profileRepository, + PermissionRepository permissionRepository, + AssignmentRepository assignmentRepository) { + this.profileRepository = profileRepository; + this.permissionRepository = permissionRepository; + this.assignmentRepository = assignmentRepository; + } + + public List findAllProfiles() { return profileRepository.findAll(); } + + public Profile getProfile(String userName) { + log.debug("get Profile {}", userName); + return profileRepository.findByUserName(userName); + } + + public List findAllPermissions() { return permissionRepository.findAll(); } + + public Collection findAllPermissions(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return permissionRepository.findAll(); + } else { + return permissionRepository.search(stringFilter); + } + } + + public Permission addPermission(String permissionName) { + Permission permission = permissionRepository.findByName(permissionName); + if (permission == null) { + log.info("Permission {} was not found, will create it.", permissionName); + permission = new Permission(); + permission.setName(permissionName); + permissionRepository.save(permission); + } + return permission; + } + + public void savePermission(Permission permission) { + if (permission == null) { + log.warn("Permission is null. Can't save it."); + } + log.info("savePermission: permission={}", permission); + permissionRepository.save(permission); + } + + public void deletePermission(Permission permission) { + if (permission == null) { + log.warn("Permission is null. Can't delete it."); + return; + } + log.info("deletePermission: permission={}", permission); + permissionRepository.delete(permission); + } + + public List findAllAssignments() { + return assignmentRepository.findAll(); + } + + public void saveAssignment(Assignment assignment) { + if (assignment == null) { + log.warn("Assignment is null. Can't save it."); + return; + } + log.info("saveAssignment: assignment={}", assignment); + assignmentRepository.save(assignment); + } + + public void deleteAssignment(Assignment assignment) { + if (assignment == null) { + log.warn("Assignment is null. Can't delete it."); + return; + } + log.info("deleteAssignment: assignment={}", assignment); + assignmentRepository.delete(assignment); + } + + public String getProfileFullName(String userName) { + log.debug("get Fullname für Profile {}", userName); + Profile profile = profileRepository.findByUserName(userName); + if (profile == null) { + log.info("keinen Eintrag für {} gefunden", userName); + return userName; + } else { + log.info("Voller Name des Profile {}: {}", userName, profile.getFullName()); + return profile.getFullName(); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java new file mode 100644 index 0000000..52fc79a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/KontorUserDetailsService.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.*; +import de.thpeetz.kontor.admin.repository.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service("userDetailsService") +public class KontorUserDetailsService implements UserDetailsService { + + private static SecureRandom random = new SecureRandom(); + + @Autowired + private ProfileRepository profileRepository; + + @Autowired + private PermissionRepository permissionRepository; + + @Autowired + private AssignmentRepository assignmentRepository; + + public Collection findAllProfiles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return profileRepository.findAll(); + } else { + return profileRepository.search(stringFilter); + } + } + + public void saveProfile(Profile profile) { + if (profile == null) { + log.warn("Profile is null. Can't save it."); + return; + } + log.info("saveProfile: profile={}", profile); + profileRepository.save(profile); + } + + public void saveProfile(Profile profile, List permissions) { + if (profile == null) { + log.warn("Profile is null. Can't save it."); + return; + } + log.info("First save Profile: {}", profile); + profile = profileRepository.save(profile); + List copy = permissions.stream().collect(Collectors.toList()); + List assignments = profile.getAssignments(); + assignments.forEach(assignment -> { + if (permissions.contains(assignment.getPermission())) { + log.info("Permission {} already assigned", assignment.getPermission()); + copy.remove(assignment.getPermission()); + } else { + log.info("Permission {} has to be removed", assignment.getPermission()); + assignmentRepository.delete(assignment); + } + }); + log.info("remaining roles: {}", copy); + for (Permission permission : copy) { + Assignment assignment = new Assignment(); + assignment.setProfile(profile); + assignment.setPermission(permission); + assignmentRepository.save(assignment); + } + } + + public void deleteProfile(Profile profile) { + if (profile == null) { + log.warn("Profile is null. Can't delete it."); + return; + } + log.info("deleteProfile: profile={}", profile); + profileRepository.delete(profile); + } + + @Override + public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException { + + log.info("loadUserByUsername: userName={}", userName); + Profile profile = profileRepository.findByUserName(userName); + if (profile == null) { + log.info("User not found"); + return null; + } + + Collection authorities = getAuthorities(profile); + log.info("Profile {} hat Permissions: {}", userName, authorities); + return new org.springframework.security.core.userdetails.User(profile.getUserName(), profile.getPassword(), + authorities); + } + + private Collection getAuthorities(Profile profile) { + return assignmentRepository.findByProfile(profile).stream() + .map(assignment -> assignment.getPermission().getName()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + public String getRememberedUser(String id) { + log.info("getRememberedUser: id={}", id); + return "admin"; + } + + public String rememberUser(String username) { + String randomId = new BigInteger(130, random).toString(32); + log.info("rememberUser: username={}", username); + return randomId; + } + + public void removeRememberedUser(String id) { + log.info("removeRememberedUser: id={}", id); + } + + public List findAllPermissions() { + return permissionRepository.findAll(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MailService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MailService.java new file mode 100644 index 0000000..d817197 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MailService.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.repository.MailAccountRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class MailService { + + private final MailAccountRepository mailAccountRepository; + + public MailService(MailAccountRepository mailAccountRepository) { + this.mailAccountRepository = mailAccountRepository; + } + + public List findAllMailAccounts() { + return mailAccountRepository.findAll(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java new file mode 100644 index 0000000..6c45feb --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/MetaDataService.java @@ -0,0 +1,243 @@ +package de.thpeetz.kontor.admin.services; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.repository.MetaDataColumnRepository; +import de.thpeetz.kontor.admin.data.MetaDataTable; +import de.thpeetz.kontor.admin.repository.MetaDataTableRepository; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@Service +public class MetaDataService { + + private final MetaDataTableRepository metaDataTableRepository; + + private final MetaDataColumnRepository metaDataColumnRepository; + + public MetaDataService(MetaDataTableRepository metaDataTableRepository, MetaDataColumnRepository metaDataColumnRepository) { + this.metaDataTableRepository = metaDataTableRepository; + this.metaDataColumnRepository = metaDataColumnRepository; + } + + public List findAllTables() { + return metaDataTableRepository.findAll(); + } + + public MetaDataTable getTable(String tableName) { + MetaDataTable table = metaDataTableRepository.findByTableName(tableName); + if (table == null) { + log.info("Metadata for table {} not found, will create it", tableName); + table = new MetaDataTable(); + table.setTableName(tableName); + metaDataTableRepository.save(table); + } + return table; + } + + private void deleteTable(MetaDataTable metaDataTable) { + List columns = metaDataTable.getTableColumns(); + List columnsToDelete = new LinkedList<>(); + for (MetaDataColumn column: columns) { + try { + columnsToDelete.add(column); + metaDataColumnRepository.delete(column); + } catch (Exception e) { + log.info("Exception {} thrown, just go on", e.getMessage()); + } + } + for (MetaDataColumn column: columnsToDelete) { + metaDataTable.getTableColumns().remove(column); + } + try { + metaDataTableRepository.delete(metaDataTable); + } catch (Exception e) { + log.info("could not delete MetaDataTable: {}", e.getMessage()); + } + } + + public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder, Boolean isShown, String columnLabel, Boolean showFilter, String filterLabel) { + this.getColumn(table, columnName, columnSyncName, columnType, columnModifier, columnOrder, isShown, columnLabel, showFilter, filterLabel, null); + } + + public void getColumn(MetaDataTable table, String columnName, String columnSyncName, String columnType, String columnModifier, Integer columnOrder, Boolean isShown, String columnLabel, Boolean showFilter, String filterLabel, String refColumn) { + if (table.getTableColumns().stream().anyMatch(column -> column.getColumnName().equals(columnName))) { + log.debug("Column {} with name {} of table {} found, check Values", columnOrder, columnName, table.getTableName()); + MetaDataColumn column = table.getTableColumns().get(columnOrder.intValue()-1); + if (!column.getColumnName().equals(columnName)) { + log.debug("columnName has to be changed to {}", columnName); + column.setColumnName(columnName); + } + if (!column.getColumnSyncName().equals(columnSyncName)) { + log.debug("columnSyncName has to be changed to {}", columnSyncName); + column.setColumnSyncName(columnSyncName); + } + if (!column.getColumnType().equals(columnType)) { + log.debug("columnType has to be changed to {}", columnType); + column.setColumnType(columnType); + } + if (columnModifier != null && !columnModifier.equals(column.getColumnModifier())) { + log.debug("columnModifier has to be changed to {}", columnModifier); + column.setColumnModifier(columnModifier); + } + if (isShown != null && !isShown.equals(column.getIsShown())) { + log.debug("isShown has to be change to {}}", isShown); + column.setIsShown(isShown); + } + if (columnLabel != null && !columnLabel.equals(column.getColumnLabel())) { + log.debug("columnLabel has to be change to {}}", columnLabel); + column.setColumnLabel(columnLabel); + } + if (showFilter != null &&!showFilter.equals(column.getShowFilter())) { + log.debug("showFilter has to be change to {}}", showFilter); + column.setShowFilter(showFilter); + } + if (filterLabel != null && !filterLabel.equals(column.getFilterLabel())) { + log.debug("filterLabel has to be change to {}}", filterLabel); + column.setFilterLabel(filterLabel); + } + if (refColumn != null && !refColumn.equals(column.getRefColumn())) { + log.debug("refColumn has to be change to {}}", filterLabel); + column.setRefColumn(refColumn); + } + metaDataColumnRepository.save(column); + } else { + log.info("Column {} of table {} not found, will create it", columnName, table.getTableName()); + MetaDataColumn column = new MetaDataColumn(); + column.setTable(table); + column.setColumnName(columnName); + column.setColumnSyncName(columnSyncName); + column.setColumnType(columnType); + column.setColumnModifier(columnModifier); + column.setColumnOrder(columnOrder); + column.setIsShown(Boolean.FALSE); + metaDataColumnRepository.save(column); + } + } + + + public List findAllMetaDataColumns(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.debug("Found " + metaDataColumnRepository.count()+ " entries"); + return metaDataColumnRepository.findAll(); + } else { + List results = metaDataColumnRepository.search(stringFilter); + log.debug("Found " + results.size() + " entries"); + return results; + } + } + + public void deleteMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't delete it"); + return; + } + log.debug("deleteMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.delete(metaDataColumn); + } + + public void saveMetaDataColumn(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + log.warn("MetaDataColumn is null, can't save it"); + return; + } + log.debug("saveMetaDataColumn: MetaDataColumn={}", metaDataColumn); + metaDataColumnRepository.save(metaDataColumn); + } + + public String importTableData(Map fields) { + AtomicReference status = new AtomicReference<>("unknown"); + String id = fields.get("id"); + Optional optional = metaDataTableRepository.findById(id); + if (optional.isEmpty()) { + log.info(" not found: {} with {}", id, fields); + status.set(id + "not found"); + MetaDataTable checkExisting = metaDataTableRepository.findByTableName(fields.get("table_name")); + if (checkExisting != null) { + log.info("entry already there with different id ({}), will be deleted", checkExisting.getId()); + deleteTable(checkExisting); + } + MetaDataTable metaDataTable = new MetaDataTable(); + metaDataTable.setId(id); + metaDataTable.setTableName(fields.get("table_name")); + metaDataTableRepository.save(metaDataTable); + } else { + optional.ifPresent( entry -> { + log.info(" found: {}", entry.getTableName()); + String updateStatus = updateTableFields(entry, fields); + metaDataTableRepository.save(entry); + status.set(updateStatus); + }); + } + return status.get(); + } + + public String importColumnData(String tableName, Map fields) { + AtomicReference status = new AtomicReference<>("unknown"); + String id = fields.get("id"); + Optional optional = metaDataColumnRepository.findById(id); + if (optional.isEmpty()) { + log.info(" not found: {} with {}", id, fields); + status.set(id + "not found"); + MetaDataColumn metaDataColumn = new MetaDataColumn(); + metaDataColumn.setId(id); + metaDataColumn.setColumnName(fields.get("column_name")); + metaDataColumn.setColumnSyncName(fields.get("column_sync_name")); + metaDataColumn.setColumnType(fields.get("column_type")); + metaDataColumnRepository.save(metaDataColumn); + } else { + optional.ifPresent( entry -> { + log.info(" found: {}", entry.getTableName()); + String updateStatus = updateColumnFields(entry, tableName, fields); + metaDataColumnRepository.save(entry); + status.set(updateStatus); + }); + } + return status.get(); + } + + private String updateColumnFields(MetaDataColumn metaDataColumn, String tableName, Map fields) { + StringBuilder status = new StringBuilder(); + for (Map.Entry entry : fields.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + switch (key) { + case "id", "created_date", "last_modified_date", "version": + break; + case "column_name": + status.append(metaDataColumn.updateColumnName(value)); + break; + case "column_sync_name": + status.append(metaDataColumn.updateColumnSyncName(value)); + break; + default: + log.info("field {} is unknown for table {}", key, tableName); + } + } + return status.toString(); + } + + private String updateTableFields(MetaDataTable metaDataTable, Map fields) { + String status = ""; + for (Map.Entry entry : fields.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + switch (key) { + case "id", "created_date", "last_modified_date", "version": + break; + case "table_name": + status += metaDataTable.updateTableName(value); + default: + log.info("field {} is unknown for table {}", key, MetaDataTable.class.getName()); + } + } + return status; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java new file mode 100644 index 0000000..8c5f929 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/services/ModuleService.java @@ -0,0 +1,76 @@ +package de.thpeetz.kontor.admin.services; + +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.repository.ModuleDataRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class ModuleService { + + private final ModuleDataRepository moduleDataRepository; + + public ModuleService(ModuleDataRepository moduleDataRepository) { + this.moduleDataRepository = moduleDataRepository; + } + + public List findAll(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return moduleDataRepository.findAll(); + } else { + return moduleDataRepository.search(stringFilter); + } + } + + public ModuleData findByName(String moduleName) { + if (moduleName == null || moduleName.isEmpty()) { + return null; + } else { + return moduleDataRepository.findByModuleName(moduleName); + } + } + + public boolean importData(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module != null) { + return module.getImportData(); + } else { + log.info("Module {} not found, should import data", moduleName); + return true; + } + } + + public void setDataImported(String moduleName) { + ModuleData module = moduleDataRepository.findByModuleName(moduleName); + if (module == null) { + log.info("Module {} not found, will create it", moduleName); + module = new ModuleData(); + module.setModuleName(moduleName); + module.setImportData(false); + moduleDataRepository.save(module); + } else { + log.info("Module {} found, change import data", module); + module.setImportData(false); + moduleDataRepository.save(module); + } + } + + public void saveModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't save it."); + } else { + moduleDataRepository.save(moduleData); + } + } + + public void deleteModuleData(ModuleData moduleData) { + if (moduleData == null) { + log.warn("ModuleData is null, can't delete it."); + } else { + moduleDataRepository.delete(moduleData); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java new file mode 100644 index 0000000..fa6dbe7 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AdminLayout.java @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AdminLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public AdminLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(AdminConstants.ADMIN_TITLE); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(AdminConstants.getProfileNavigation(), AdminConstants.getPermissionNavigation(), + AdminConstants.getAuthorizationNavigation()); + return navigation; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentForm.java new file mode 100644 index 0000000..3dcd115 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentForm.java @@ -0,0 +1,112 @@ +package de.thpeetz.kontor.admin.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.admin.data.Assignment; +import de.thpeetz.kontor.admin.data.Permission; +import de.thpeetz.kontor.admin.data.Profile; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AssignmentForm extends FormLayout { + + ComboBox profile = new ComboBox<>("Proile"); + ComboBox permission = new ComboBox<>("Permission"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Assignment.class); + + public AssignmentForm(List profiles, List permissions) { + addClassName("assignment-form"); + binder.bindInstanceFields(this); + + profile.setItems(profiles); + profile.setItemLabelGenerator(Profile::getUserName); + permission.setItems(permissions); + permission.setItemLabelGenerator(Permission::getName); + add(profile, permission, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setAssignment(Assignment assignment) { + binder.setBean(assignment); + } + + public abstract static class AssignmentFormEvent extends ComponentEvent { + private Assignment assignment; + + protected AssignmentFormEvent(AssignmentForm source, Assignment assignment) { + super(source, false); + this.assignment = assignment; + } + + public Assignment getAssignment() { + return assignment; + } + } + + public static class SaveEvent extends AssignmentFormEvent { + SaveEvent(AssignmentForm source, Assignment assignment) { + super(source, assignment); + } + } + + public static class DeleteEvent extends AssignmentFormEvent { + DeleteEvent(AssignmentForm source, Assignment assignment) { + super(source, assignment); + } + } + + public static class CloseEvent extends AssignmentFormEvent { + CloseEvent(AssignmentForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentView.java new file mode 100644 index 0000000..bc4e547 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/AssignmentView.java @@ -0,0 +1,114 @@ +package de.thpeetz.kontor.admin.views; + +import de.thpeetz.kontor.admin.data.Assignment; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; + +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.ASSIGNMENT_ROUTE, layout = MainLayout.class) +@PageTitle("Authorization | Admin | Kontor") +public class AssignmentView extends VerticalLayout { + + Grid grid = new Grid<>(Assignment.class); + AssignmentForm form; + AdminService service; + + public AssignmentView(AdminService service) { + this.service = service; + addClassName("assignment-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("assignment-grid"); + grid.setSizeFull(); + grid.setColumns("profile.userName", "permission.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAssignment(event.getValue())); + } + + private void configureForm() { + form = new AssignmentForm(service.findAllProfiles(), service.findAllPermissions()); + form.setWidth("25em"); + form.addSaveListener(this::saveAssignment); + form.addDeleteListener(this::deleteAssignment); + form.addCloseListener(e -> closeEditor()); + } + + private void saveAssignment(AssignmentForm.SaveEvent event) { + Assignment assignment = event.getAssignment(); + service.saveAssignment(assignment); + updateList(); + closeEditor(); + } + + private void deleteAssignment(AssignmentForm.DeleteEvent event) { + service.deleteAssignment(event.getAssignment()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addAuthorizationMaxtrixButton = new Button("Add permission", click -> addAssignment()); + HorizontalLayout toolbar = new HorizontalLayout(addAuthorizationMaxtrixButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAssignment(Assignment assignment) { + if (assignment == null) { + closeEditor(); + } else { + form.setAssignment(assignment); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setAssignment(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAssignment() { + grid.asSingleSelect().clear(); + editAssignment(new Assignment()); + } + + private void updateList() { + grid.setItems(service.findAllAssignments()); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java new file mode 100644 index 0000000..286bc03 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/LoginView.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.admin.views; + +import org.springframework.security.core.context.SecurityContextHolder; + +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.login.LoginForm; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.BeforeEnterEvent; +import com.vaadin.flow.router.BeforeEnterObserver; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route("login") +@PageTitle("Login | Vaadin Kontor") +@AnonymousAllowed +public class LoginView extends VerticalLayout implements BeforeEnterObserver { + + private final LoginForm login = new LoginForm(); + + public LoginView() { + addClassName("login-view"); + setSizeFull(); + setAlignItems(Alignment.CENTER); + setJustifyContentMode(JustifyContentMode.CENTER); + + login.setAction("login"); + + add(new H1("Vaadin Kontor")); + add(new Span("Username: user, Password: password")); + add(new Span("Username: admin, Password: password")); + add(login); + } + + @Override + public void beforeEnter(BeforeEnterEvent beforeEnterEvent) { + log.info("beforeEnter: {}", beforeEnterEvent.getLocation()); + log.info("beforeEnter: {}", SecurityContextHolder.getContext().getAuthentication()); + // inform the user about an authentication error + if (beforeEnterEvent.getLocation() + .getQueryParameters() + .getParameters() + .containsKey("error")) { + login.setError(true); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java new file mode 100644 index 0000000..481f237 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataForm.java @@ -0,0 +1,123 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.data.MetaDataTable; + +import java.util.List; + +public class MetaDataForm extends FormLayout { + + ComboBox table = new ComboBox<>("Table"); + TextField columnName = new TextField("Column Name"); + TextField columnSyncName = new TextField("Column Sync Name"); + TextField columnModifier = new TextField("Column Modifier"); + IntegerField columnOrder = new IntegerField("Column Order"); + Checkbox isShown = new Checkbox("Is Shown"); + TextField columnLabel = new TextField("Column Label"); + Checkbox showFilter = new Checkbox("Show Filter"); + TextField filterLabel = new TextField("Filter Label"); + TextField refColumn = new TextField("Ref Column"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MetaDataColumn.class); + + public MetaDataForm(List tables) { + addClassName("metaData-form"); + binder.bindInstanceFields(this); + + table.setItems(tables); + table.setItemLabelGenerator(MetaDataTable::getTableName); + add(table, columnName, columnSyncName, columnModifier, columnOrder); + add(isShown, columnLabel); + isShown.addClickListener(click -> columnLabel.setEnabled(isShown.getValue())); + add(showFilter, filterLabel); + showFilter.addClickListener(click -> filterLabel.setEnabled(showFilter.getValue())); + add(refColumn); + add(createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MetaDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MetaDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MetaDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMetaDataColumn(MetaDataColumn metaDataColumn) { + binder.setBean(metaDataColumn); + } + + public abstract static class MetaDataFormEvent extends ComponentEvent { + private MetaDataColumn metaDataColumn; + + protected MetaDataFormEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, false); + this.metaDataColumn = metaDataColumn; + } + + public MetaDataColumn getMetaDataColumn() { + return metaDataColumn; + } + } + + public static class SaveEvent extends MetaDataForm.MetaDataFormEvent { + SaveEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class DeleteEvent extends MetaDataForm.MetaDataFormEvent { + DeleteEvent(MetaDataForm source, MetaDataColumn metaDataColumn) { + super(source, metaDataColumn); + } + } + + public static class CloseEvent extends MetaDataForm.MetaDataFormEvent { + CloseEvent(MetaDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MetaDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MetaDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MetaDataForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java new file mode 100644 index 0000000..6137f3e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/MetaDataView.java @@ -0,0 +1,193 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.provider.SortDirection; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.data.MetaDataColumn; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.StatusIcon; +import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.METADATA_ROUTE, layout = MainLayout.class) +@PageTitle("Meta Data | Admin | Kontor") +public class MetaDataView extends VerticalLayout { + + Grid grid = new Grid<>(MetaDataColumn.class, false); + Grid.Column idColumn = grid.addColumn(MetaDataColumn::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MetaDataColumn::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MetaDataColumn::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column versionColumn = grid.addColumn(MetaDataColumn::getVersion) + .setHeader("Version").setResizable(true).setSortable(true); + Grid.Column tableColumn = grid.addColumn(MetaDataColumn::getTableName) + .setHeader("Table").setResizable(true).setSortable(true); + Grid.Column columnNameColumn = grid.addColumn(MetaDataColumn::getColumnName) + .setHeader("Column Name").setResizable(true).setSortable(true); + Grid.Column columnSyncNameColumn = grid.addColumn(MetaDataColumn::getColumnSyncName) + .setHeader("Column Sync Name").setResizable(true).setSortable(true); + Grid.Column columnTypeColumn = grid.addColumn(MetaDataColumn::getColumnType) + .setHeader("Column Type").setResizable(true).setSortable(true); + Grid.Column columnModifierColumn = grid.addColumn(MetaDataColumn::getColumnModifier) + .setHeader("Column Modifier").setResizable(true).setSortable(true); + Grid.Column columnOrderColumn = grid.addColumn(MetaDataColumn::getColumnOrder) + .setHeader("Column Order").setResizable(true).setSortable(true); + Grid.Column isShownColumn = grid.addComponentColumn(metaDataColumn -> StatusIcon.create(metaDataColumn.getIsShown())). + setHeader("Anzeige?").setWidth("6rem").setSortable(true); + Grid.Column columnLabelColumn = grid.addColumn(MetaDataColumn::getColumnLabel) + .setHeader("Spaltenname").setResizable(true).setSortable(true); + Grid.Column showFilterColumn = grid.addComponentColumn(metaDataColumn -> StatusIcon.create(metaDataColumn.getShowFilter())). + setHeader("Zeige Filter").setWidth("6rem").setSortable(true); + Grid.Column filterLabelColumn = grid.addColumn(MetaDataColumn::getFilterLabel) + .setHeader("Filter Name").setResizable(true).setSortable(true); + Grid.Column refColumnColumn = grid.addColumn(MetaDataColumn::getRefColumn) + .setHeader("Ref Column Name").setResizable(true).setSortable(true); + TextField searchField = new TextField(); + @Getter + MetaDataForm form; + MetaDataService service; + + public MetaDataView(MetaDataService service) { + this.service = service; + addClassName("metadata-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("metadata-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + createdColumn.setVisible(false); + modifiedColumn.setVisible(false); + versionColumn.setVisible(false); + grid.setMultiSort(true); + List> sortOrder = new ArrayList<>(); + sortOrder.add(new GridSortOrder(tableColumn, SortDirection.ASCENDING)); + sortOrder.add(new GridSortOrder(columnOrderColumn, SortDirection.ASCENDING)); + grid.sort(sortOrder); + grid.asSingleSelect().addValueChangeListener(event -> editMetaData(event.getValue())); + } + + private void configureForm() { + form = new MetaDataForm(service.findAllTables()); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMetaData); + form.addDeleteListener(this::deleteMetaData); + form.addCloseListener(e -> closeEditor()); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMetaDataButton = new Button("Add Meta Data"); + addMetaDataButton.addClickListener(click -> addMetaDataColumn()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(createdColumn); + columnToggleContextMenu.addColumnToggleItem(modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(versionColumn); + columnToggleContextMenu.addColumnToggleItem(tableColumn); + columnToggleContextMenu.addColumnToggleItem(columnNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnSyncNameColumn); + columnToggleContextMenu.addColumnToggleItem(columnTypeColumn); + columnToggleContextMenu.addColumnToggleItem(columnModifierColumn); + columnToggleContextMenu.addColumnToggleItem(columnOrderColumn); + columnToggleContextMenu.addColumnToggleItem(isShownColumn); + columnToggleContextMenu.addColumnToggleItem(columnLabelColumn); + columnToggleContextMenu.addColumnToggleItem(showFilterColumn); + columnToggleContextMenu.addColumnToggleItem(filterLabelColumn); + columnToggleContextMenu.addColumnToggleItem(refColumnColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMetaDataButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + private void saveMetaData(MetaDataForm.SaveEvent event) { + MetaDataColumn metaDataColumn = event.getMetaDataColumn(); + service.saveMetaDataColumn(metaDataColumn); + updateList(); + closeEditor(); + } + + private void deleteMetaData(MetaDataForm.DeleteEvent event) { + service.deleteMetaDataColumn(event.getMetaDataColumn()); + updateList(); + closeEditor(); + } + + public void editMetaData(MetaDataColumn metaDataColumn) { + if (metaDataColumn == null) { + closeEditor(); + } else { + form.setMetaDataColumn(metaDataColumn); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setMetaDataColumn(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMetaDataColumn() { + grid.asSingleSelect().clear(); + editMetaData(new MetaDataColumn()); + } + + private void updateList() { + grid.setItems(service.findAllMetaDataColumns(searchField.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java new file mode 100644 index 0000000..b2c5f8c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.ModuleData; + +public class ModuleDataForm extends FormLayout { + TextField moduleName = new TextField("Module Name"); + Checkbox importData = new Checkbox("Import Data"); + + Button save = new com.vaadin.flow.component.button.Button("Save"); + Button delete = new com.vaadin.flow.component.button.Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ModuleData.class); + + public ModuleDataForm() { + addClassName("moduleData-form"); + binder.bindInstanceFields(this); + add(moduleName, importData, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ModuleDataForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ModuleDataForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ModuleDataForm.SaveEvent(this, binder.getBean())); + } + } + + public void setModuleData(ModuleData moduleData) { + binder.setBean(moduleData); + } + + public abstract static class ModuleDataFormEvent extends ComponentEvent { + private ModuleData moduleData; + + protected ModuleDataFormEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, false); + this.moduleData = moduleData; + } + + public ModuleData getModuleData() { + return moduleData; + } + } + + public static class SaveEvent extends ModuleDataForm.ModuleDataFormEvent { + SaveEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class DeleteEvent extends ModuleDataForm.ModuleDataFormEvent { + DeleteEvent(ModuleDataForm source, ModuleData moduleData) { + super(source, moduleData); + } + } + + public static class CloseEvent extends ModuleDataForm.ModuleDataFormEvent { + CloseEvent(ModuleDataForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ModuleDataForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ModuleDataForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ModuleDataForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java new file mode 100644 index 0000000..89f93f1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ModuleDataView.java @@ -0,0 +1,140 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.ModuleData; +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.StatusIcon; +import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/module", layout = MainLayout.class) +@PageTitle("Module Data | Admin | Kontor") +public class ModuleDataView extends VerticalLayout { + + Grid grid = new Grid<>(ModuleData.class, false); + Grid.Column idColumn = grid.addColumn(ModuleData::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column nameColumn = grid.addColumn(ModuleData::getModuleName) + .setHeader("Name").setResizable(true).setSortable(true); + Grid.Column importColumn = grid.addComponentColumn(moduleData -> StatusIcon.create(moduleData.getImportData())) + .setHeader("Import Data").setWidth("6rem").setSortable(true); + + TextField filterText = new TextField(); + @Getter + ModuleDataForm form; + ModuleService service; + + public ModuleDataView(ModuleService service) { + this.service = service; + addClassName("moduleData-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("moduleData-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editModuleData(event.getValue())); + } + + private void configureForm() { + form = new ModuleDataForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveModuleData); + form.addDeleteListener(this::deleteModuleData); + form.addCloseListener(e -> closeEditor()); + } + + private void saveModuleData(ModuleDataForm.SaveEvent event) { + ModuleData moduleData = event.getModuleData(); + service.saveModuleData(moduleData); + updateList(); + closeEditor(); + } + + private void deleteModuleData(ModuleDataForm.DeleteEvent event) { + service.deleteModuleData(event.getModuleData()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by module name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addModuleDataButton = new Button("Add module", click -> addModuleData()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(nameColumn); + columnToggleContextMenu.addColumnToggleItem(importColumn); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addModuleDataButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editModuleData(ModuleData moduleData) { + if (moduleData == null) { + closeEditor(); + } else { + form.setModuleData(moduleData); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setModuleData(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addModuleData() { + grid.asSingleSelect().clear(); + editModuleData(new ModuleData()); + } + + private void updateList() { + grid.setItems(service.findAll(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionForm.java new file mode 100644 index 0000000..521c992 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionForm.java @@ -0,0 +1,101 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.Permission; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PermissionForm extends FormLayout { + + TextField name = new TextField("Role name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Permission.class); + + public PermissionForm() { + addClassName("permission-form"); + binder.bindInstanceFields(this); + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPermission(Permission permission) { + binder.setBean(permission); + } + + public abstract static class PermissionFormEvent extends ComponentEvent { + private Permission permission; + + protected PermissionFormEvent(PermissionForm source, Permission permission) { + super(source, false); + this.permission = permission; + } + + public Permission getPermission() { + return permission; + } + } + + public static class SaveEvent extends PermissionFormEvent { + SaveEvent(PermissionForm source, Permission permission) { + super(source, permission); + } + } + + public static class DeleteEvent extends PermissionFormEvent { + DeleteEvent(PermissionForm source, Permission permission) { + super(source, permission); + } + } + + public static class CloseEvent extends PermissionFormEvent { + CloseEvent(PermissionForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionView.java new file mode 100644 index 0000000..f23881d --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/PermissionView.java @@ -0,0 +1,119 @@ +package de.thpeetz.kontor.admin.views; + +import de.thpeetz.kontor.admin.data.Permission; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = AdminConstants.PERMISSION_ROUTE, layout = MainLayout.class) +@PageTitle("Permissions | Admin | Kontor") +public class PermissionView extends VerticalLayout { + + Grid grid = new Grid<>(Permission.class); + TextField filterText = new TextField(); + PermissionForm form; + AdminService service; + + public PermissionView(AdminService service) { + this.service = service; + addClassName("permission-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("permission-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPermission(event.getValue())); + } + + private void configureForm() { + form = new PermissionForm(); + form.setWidth("25em"); + form.addSaveListener(this::savePermission); + form.addDeleteListener(this::deletePermission); + form.addCloseListener(e -> closeEditor()); + } + + private void savePermission(PermissionForm.SaveEvent event) { + service.savePermission(event.getPermission()); + updateList(); + closeEditor(); + } + + private void deletePermission(PermissionForm.DeleteEvent event) { + service.deletePermission(event.getPermission()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addUserButton = new Button("Add user", click -> addPermission()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addUserButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPermission(Permission permission) { + if (permission == null) { + closeEditor(); + } else { + form.setPermission(permission); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setPermission(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPermission() { + grid.asSingleSelect().clear(); + editPermission(new Permission()); + } + + private void updateList() { + grid.setItems(service.findAllPermissions(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileForm.java new file mode 100644 index 0000000..cb3166c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileForm.java @@ -0,0 +1,143 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.checkbox.CheckboxGroup; +import com.vaadin.flow.component.checkbox.CheckboxGroupVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.EmailField; +import com.vaadin.flow.component.textfield.PasswordField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.admin.data.Permission; +import de.thpeetz.kontor.admin.data.Profile; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class ProfileForm extends FormLayout { + + TextField userName = new TextField("User name"); + PasswordField password = new PasswordField("Password"); + EmailField email = new EmailField("Email"); + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + Checkbox enabled = new Checkbox("Enabled"); + String originalPassword; + + CheckboxGroup permissionList = new CheckboxGroup<>("Permissions"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Profile.class); + + public ProfileForm() { + addClassName("profile-form"); + binder.bindInstanceFields(this); + add(userName, password, email, firstName, lastName, enabled, configurePermissionsGroup(), createButtonsLayout()); + } + + private CheckboxGroup configurePermissionsGroup() { + permissionList.addThemeVariants(CheckboxGroupVariant.LUMO_VERTICAL); + permissionList.setItemLabelGenerator(Permission::getName); + permissionList.addValueChangeListener(event -> { + log.debug("permissions changed: {}", event); + }); + return permissionList; + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ProfileForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ProfileForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ProfileForm.SaveEvent(this, binder.getBean())); + } + } + + public void setProfile(Profile profile) { + binder.setBean(profile); + //log.debug("UserForm.setUser: {}", user); + if (profile != null) { + this.originalPassword = profile.getPassword(); + } else { + this.originalPassword = null; + } + } + + public void setPermissions(List permissions, Profile profile) { + permissionList.setItems(permissions); + profile.getAssignments().stream().forEach(assignment -> { + permissionList.select(assignment.getPermission()); + }); + } + + public boolean hasPasswordChanged(Profile profile) { + return !originalPassword.equals(profile.getPassword()); + } + + public abstract static class ProfileFormEvent extends ComponentEvent { + private Profile profile; + + protected ProfileFormEvent(ProfileForm source, Profile profile) { + super(source, false); + this.profile = profile; + } + + public Profile getProfile() { + return profile; + } + } + + public static class SaveEvent extends ProfileForm.ProfileFormEvent { + SaveEvent(ProfileForm source, Profile profile) { + super(source, profile); + } + } + + public static class DeleteEvent extends ProfileForm.ProfileFormEvent { + DeleteEvent(ProfileForm source, Profile profile) { + super(source, profile); + } + } + + public static class CloseEvent extends ProfileForm.ProfileFormEvent { + CloseEvent(ProfileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ProfileForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ProfileForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ProfileForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileView.java new file mode 100644 index 0000000..16bc6b0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/ProfileView.java @@ -0,0 +1,135 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.admin.data.Permission; +import de.thpeetz.kontor.admin.data.Profile; +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.RolesAllowed; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("ROLE_ADMIN") +@Route(value = "admin/profile", layout = MainLayout.class) +@PageTitle("Profile | Admin | Kontor") +public class ProfileView extends VerticalLayout { + + Grid grid = new Grid<>(Profile.class); + TextField filterText = new TextField(); + ProfileForm form; + KontorUserDetailsService service; + + @Autowired + PasswordEncoder passwordEncoder; + + public ProfileView(KontorUserDetailsService service) { + this.service = service; + addClassName("profile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("profile-grid"); + grid.setSizeFull(); + grid.setColumns("userName", "email", "firstName", "lastName", "enabled"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editProfile(event.getValue())); + } + + private void configureForm() { + form = new ProfileForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveProfile); + form.addDeleteListener(this::deleteProfile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveProfile(ProfileForm.SaveEvent event) { + Profile profile = event.getProfile(); + log.debug("ProfileView.saveProfile: {}", profile); + List permissions = form.permissionList.getSelectedItems().stream().collect(Collectors.toList()); + log.info("selected permissions: {}", permissions); + if (form.hasPasswordChanged(profile)) { + profile.setPassword(passwordEncoder.encode(profile.getPassword())); + log.debug("password changed for profile {}", profile); + } + service.saveProfile(profile, permissions); + updateList(); + closeEditor(); + } + + private void deleteProfile(ProfileForm.DeleteEvent event) { + service.deleteProfile(event.getProfile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by user name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + Button addProfileButton = new Button("Add profile", click -> addProfile()); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addProfileButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editProfile(Profile profile) { + if (profile == null) { + closeEditor(); + } else { + form.setProfile(profile); + form.setPermissions(service.findAllPermissions(), profile); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setProfile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addProfile() { + grid.asSingleSelect().clear(); + editProfile(new Profile()); + } + + private void updateList() { + grid.setItems(service.findAllProfiles(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java new file mode 100644 index 0000000..ef00bf2 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/admin/views/UserProfileView.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.admin.views; + +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import de.thpeetz.kontor.admin.data.Profile; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.security.SecurityService; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Route(value="user/profile", layout = MainLayout.class) +@PermitAll +@PageTitle("Profile | User | Kontor") +public class UserProfileView extends VerticalLayout { + + private SecurityService securityService; + + private AdminService adminService; + + TextField firstName = new TextField("First name"); + TextField lastName = new TextField("Last name"); + + Button save = new Button("Save"); + Button close = new Button("Cancel"); + Binder binder = new BeanValidationBinder<>(Profile.class); + + public UserProfileView(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + binder.bindInstanceFields(this); + + add(firstName, lastName, createButtonsLayout()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("UserProfileView: {}", user.getUsername()); + binder.setBean(adminService.getProfile(user.getUsername())); + }); + + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + close.addClickListener(event -> closeView()); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + log.info("update user profile"); + } + } + + private void closeView() { + log.info("close user profile view"); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java new file mode 100644 index 0000000..58661a0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/BookshelfConstants.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.bookshelf; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.bookshelf.views.ArticleView; +import de.thpeetz.kontor.bookshelf.views.AuthorView; +import de.thpeetz.kontor.bookshelf.views.BookView; +import de.thpeetz.kontor.bookshelf.views.BookshelfPublisherView; + +public class BookshelfConstants { + + public static final String AUTHOR = "Author"; + public static final String ARTICLE = "Article"; + public static final String BOOK = "Book"; + public static final String BOOKSHELF = "Bookshelf"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "bookshelf/publisher"; + public static final String AUTHOR_ROUTE = "bookshelf/author"; + public static final String BOOK_ROUTE = "bookshelf/book"; + public static final String ARTICLE_ROUTE = "bookshelf/article"; + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, BookshelfPublisherView.class); + } + + public static RouterLink getAuthorLink() { + return new RouterLink(AUTHOR, AuthorView.class); + } + + public static RouterLink getBookLink() { + return new RouterLink(BOOK, BookView.class); + } + + public static RouterLink getArticleLink() { + return new RouterLink(ARTICLE, ArticleView.class); + } + + public static SideNavItem getBookshelfNavigation() { + SideNavItem bookshelf = new SideNavItem(BOOKSHELF, BookView.class, VaadinIcon.OPEN_BOOK.create()); + bookshelf.addItem(new SideNavItem(BOOK, BookView.class)); + bookshelf.addItem(new SideNavItem(PUBLISHER, BookshelfPublisherView.class)); + bookshelf.addItem(new SideNavItem(AUTHOR, AuthorView.class)); + bookshelf.addItem(new SideNavItem(ARTICLE, ArticleView.class)); + return bookshelf; + } + + private BookshelfConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java new file mode 100644 index 0000000..abb0e9a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/SetupModuleBookshelf.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.bookshelf.data.ArticleAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.AuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookAuthorRepository; +import de.thpeetz.kontor.bookshelf.data.BookRepository; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisherRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleBookshelf implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Autowired + private AuthorRepository authorRepository; + + @Autowired + private ArticleAuthorRepository articleAuthorRepository; + + @Autowired + private BookRepository bookRepository; + + @Autowired + private BookAuthorRepository bookAuthorRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleBookshelf already executed, skipping"); + return; + } + if (!moduleService.importData(BookshelfConstants.BOOKSHELF)) { + log.info("Module Bookshelf should not setup data"); + return; + } + + log.info("Set up Bookshelf data"); + Author douglasadams = createAuthorIfNotFound("Douglas", "Adams"); + moduleService.setDataImported(BookshelfConstants.BOOKSHELF); + } + + private Author createAuthorIfNotFound(String firstName, String lastName) { + log.info("createAuthorIfNotFound {} {}", firstName, lastName); + Author author = authorRepository.findByFirstNameAndLastName(firstName, lastName); + if (author == null) { + log.info("Author {} {} not found, will create it", firstName, lastName); + author = new Author(); + author.setFirstName(firstName); + author.setLastName(lastName); + authorRepository.save(author); + } + return author; + } + + private BookshelfPublisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + BookshelfPublisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new BookshelfPublisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java new file mode 100644 index 0000000..3ee6a16 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Article.java @@ -0,0 +1,27 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title"), uniqueConstraints = @UniqueConstraint(columnNames = {"title"})) +public class Article extends AbstractEntity { + + @NotEmpty + private String title; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "article") + @Nullable + private List authors = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java new file mode 100644 index 0000000..c2b3851 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, article_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "article_id"})) +public class ArticleAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "article_id") + @NotNull + private Article article; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java new file mode 100644 index 0000000..d7933de --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByArticle(Article article); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java new file mode 100644 index 0000000..c6b2602 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/ArticleRepository.java @@ -0,0 +1,16 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArticleRepository extends JpaRepository { + + @Query("select a from Article a " + + "where lower(a.title) like lower(concat('%', :searchTerm, '%'))") + List

search(@Param("searchTerm") String searchTerm); + + List
findByTitle(String title); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java new file mode 100644 index 0000000..42d83e0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Author.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Author extends AbstractEntity { + + @NotEmpty + private String firstName; + + @NotEmpty + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List bookAuthors = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "author") + @Nullable + private List articleAuthors = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java new file mode 100644 index 0000000..3e87334 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/AuthorRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface AuthorRepository extends JpaRepository { + + @Query("select a from Author a " + + "where lower(a.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(a.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Author findByFirstNameAndLastName(String firstName, String lastName); + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java new file mode 100644 index 0000000..e0f6782 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/Book.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "title, isbn"), uniqueConstraints = @UniqueConstraint(columnNames = {"isbn"})) +public class Book extends AbstractEntity { + + @NotEmpty + private String title; + + @NotEmpty + private String isbn; + + @Nullable + private int year; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "book") + @Nullable + private List authors = new LinkedList<>(); + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "books" }) + private BookshelfPublisher publisher; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java new file mode 100644 index 0000000..86a0e10 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "author_id, book_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"author_id", "book_id"})) +public class BookAuthor extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "author_id") + @NotNull + private Author author; + + @ManyToOne + @JoinColumn(name = "book_id") + @NotNull + private Book book; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java new file mode 100644 index 0000000..a0cf650 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepository.java @@ -0,0 +1,12 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookAuthorRepository extends JpaRepository { + + List findByAuthor(Author author); + + List findByBook(Book book); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java new file mode 100644 index 0000000..e6ee013 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookRepository.java @@ -0,0 +1,21 @@ +package de.thpeetz.kontor.bookshelf.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface BookRepository extends JpaRepository { + + @Query("select b from Book b " + + "where lower(b.title) like lower(concat('%', :searchTerm, '%')) " + + "or lower(b.isbn) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + List findByTitle(String name); + + List findByTitleIgnoreCase(String name); + + List findByIsbn(String isbn); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java new file mode 100644 index 0000000..da97d7a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisher.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Entity +@Table(indexes = @Index(columnList = "name"), uniqueConstraints = @UniqueConstraint(columnNames = { "name" })) +public class BookshelfPublisher extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", orphanRemoval = true) + @Nullable + List books = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java new file mode 100644 index 0000000..b50ba84 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.bookshelf.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface BookshelfPublisherRepository extends JpaRepository { + + @Query("select p from BookshelfPublisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + BookshelfPublisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java new file mode 100644 index 0000000..6d92a2c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/services/BookshelfService.java @@ -0,0 +1,118 @@ +package de.thpeetz.kontor.bookshelf.services; + +import java.util.List; + +import de.thpeetz.kontor.bookshelf.data.*; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class BookshelfService { + + private final AuthorRepository authorRepository; + private final ArticleAuthorRepository articleAuthorRepository; + private final ArticleRepository articleRepository; + private final BookRepository bookRepository; + private final BookAuthorRepository bookAuthorRepository; + private final BookshelfPublisherRepository publisherRepository; + + public BookshelfService(AuthorRepository authorRepository, ArticleAuthorRepository articleAuthorRepository, + ArticleRepository articleRepository, BookRepository bookRepository, + BookAuthorRepository bookAuthorRepository, BookshelfPublisherRepository publisherRepository) { + this.authorRepository = authorRepository; + this.articleAuthorRepository = articleAuthorRepository; + this.articleRepository = articleRepository; + this.bookRepository = bookRepository; + this.bookAuthorRepository = bookAuthorRepository; + this.publisherRepository = publisherRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public BookshelfPublisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(BookshelfPublisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(BookshelfPublisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllAuthors(String filter) { + if (filter == null || filter.isEmpty()) { + return authorRepository.findAll(); + } else { + return authorRepository.search(filter); + } + } + + public void saveAuthor(Author author) { + if (author == null) { + log.warn("Author is null. Are you sure you have connected your form to the application?"); + return; + } + authorRepository.save(author); + } + + public void deleteAuthor(Author author) { + authorRepository.delete(author); + } + + public List findAllBooks(String filter) { + if (filter == null || filter.isEmpty()) { + return bookRepository.findAll(); + } else { + return bookRepository.search(filter); + } + } + + public void saveBook(Book book) { + if (book == null) { + log.warn("Book is null. Are you sure you have connected your form to the application?"); + return; + } + bookRepository.save(book); + } + + public void deleteBook(Book book) { + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + publisherRepository.save(publisher); + bookRepository.delete(book); + } + + public List
findAllArticles(String filter) { + if (filter == null || filter.isEmpty()) { + return articleRepository.findAll(); + } else { + return articleRepository.search(filter); + } + } + + public void saveArticle(Article article) { + if (article == null) { + log.warn("Article is null, Are you sure you have connected your form to the application?"); + return; + } + articleRepository.save(article); + } + + public void deleteArticle(Article article) { + articleRepository.delete(article); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java new file mode 100644 index 0000000..7fb7958 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleForm.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Author; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class ArticleForm extends FormLayout { + + TextField title = new TextField("Title"); + Grid author = new Grid<>(Author.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder
binder = new BeanValidationBinder<>(Article.class); + + public ArticleForm() { + addClassName("article-form"); + binder.bindInstanceFields(this); + + add(title, author, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new ArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new ArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new ArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setArticle(Article article) { + binder.setBean(article); + } + + public abstract static class ArticleFormEvent extends ComponentEvent { + private Article article; + + protected ArticleFormEvent(ArticleForm source, Article article) { + super(source, false); + this.article = article; + } + + public Article getArticle() { + return article; + } + } + + public static class SaveEvent extends ArticleForm.ArticleFormEvent { + SaveEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class DeleteEvent extends ArticleForm.ArticleFormEvent { + DeleteEvent(ArticleForm source, Article article) { + super(source, article); + } + } + + public static class CloseEvent extends ArticleForm.ArticleFormEvent { + CloseEvent(ArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(ArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(ArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(ArticleForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java new file mode 100644 index 0000000..334f577 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/ArticleView.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Article; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +import java.util.stream.Collectors; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.ARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("Article | Bookshelf | Kontor") +public class ArticleView extends VerticalLayout { + + @Getter + Grid
grid = new Grid<>(Article.class); + TextField filterText = new TextField(); + @Getter + ArticleForm form; + BookshelfService service; + + public ArticleView(BookshelfService service) { + this.service = service; + addClassName("article-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("article-grid"); + grid.setSizeFull(); + grid.setColumns("title"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArticle(event.getValue())); + } + + private void configureForm() { + form = new ArticleForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArticle); + form.addDeleteListener(this::deleteArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArticle(ArticleForm.SaveEvent event) { + service.saveArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private void deleteArticle(ArticleForm.DeleteEvent event) { + service.deleteArticle(event.getArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArticleButton = new Button("Add article"); + addArticleButton.addClickListener(click -> addArticle()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArticleButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArticle(Article article) { + if (article == null) { + closeEditor(); + } else { + form.setArticle(article); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArticle() { + grid.asSingleSelect().clear(); + editArticle(new Article()); + } + + public void updateList() { + grid.setItems(service.findAllArticles(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java new file mode 100644 index 0000000..34d0f5d --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.listbox.ListBox; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class AuthorForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + ListBox books = new ListBox<>(); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Author.class); + + public AuthorForm() { + addClassName("author-form"); + binder.bindInstanceFields(this); + + books.setHeight("100px"); + //books.setColumns("title", "publisher.name"); + //books.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, books, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new AuthorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new AuthorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new AuthorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setAuthor(Author author) { + binder.setBean(author); + } + + public void setBooks(List books) { + log.info("setting Books: ", books); + this.books.setItems(books); + } + + public abstract static class AuthorFormEvent extends ComponentEvent { + private Author author; + + protected AuthorFormEvent(AuthorForm source, Author author) { + super(source, false); + this.author = author; + } + + public Author getAuthor() { + return author; + } + } + + public static class SaveEvent extends AuthorForm.AuthorFormEvent { + SaveEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class DeleteEvent extends AuthorForm.AuthorFormEvent { + DeleteEvent(AuthorForm source, Author author) { + super(source, author); + } + } + + public static class CloseEvent extends AuthorForm.AuthorFormEvent { + CloseEvent(AuthorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(AuthorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(AuthorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(AuthorForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java new file mode 100644 index 0000000..17897b0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/AuthorView.java @@ -0,0 +1,132 @@ +package de.thpeetz.kontor.bookshelf.views; + +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.BookAuthor; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.AUTHOR_ROUTE, layout = MainLayout.class) +@PageTitle("Author | Bookshelf | Kontor") +public class AuthorView extends VerticalLayout { + + Grid grid = new Grid<>(Author.class); + TextField filterText = new TextField(); + AuthorForm form; + BookshelfService service; + + public AuthorView(BookshelfService service) { + this.service = service; + addClassName("author-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + public AuthorForm getForm() { + return form; + } + + private void configureGrid() { + grid.addClassName("author-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editAuthor(event.getValue())); + } + + private void configureForm() { + form = new AuthorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveAuthor); + form.addDeleteListener(this::deleteAuthor); + form.addCloseListener(e -> closeEditor()); + } + private void saveAuthor(AuthorForm.SaveEvent event) { + service.saveAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private void deleteAuthor(AuthorForm.DeleteEvent event) { + service.deleteAuthor(event.getAuthor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addAuthorButton = new Button("Add author"); + addAuthorButton.addClickListener(click -> addAuthor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addAuthorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editAuthor(Author author) { + if (author == null) { + closeEditor(); + } else { + form.setAuthor(author); + form.setBooks(author.getBookAuthors().stream().map(BookAuthor::getBook).collect(Collectors.toList())); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setAuthor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addAuthor() { + grid.asSingleSelect().clear(); + editAuthor(new Author()); + } + + public void updateList() { + grid.setItems(service.findAllAuthors(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java new file mode 100644 index 0000000..61da274 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookForm.java @@ -0,0 +1,111 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class BookForm extends FormLayout { + + TextField title = new TextField("Title"); + TextField isbn = new TextField("ISBN"); + IntegerField year = new IntegerField("Year"); + ComboBox publisher = new ComboBox<>("Publisher"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Book.class); + + public BookForm(List publishers) { + addClassName("book-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(BookshelfPublisher::getName); + add(title, isbn, year, publisher, createButtonsLayout()); + } + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new BookForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new BookForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new BookForm.SaveEvent(this, binder.getBean())); + } + } + + public void setBook(Book book) { + binder.setBean(book); + } + + public abstract static class BookFormEvent extends ComponentEvent { + private Book book; + + protected BookFormEvent(BookForm source, Book book) { + super(source, false); + this.book = book; + } + + public Book getBook() { + return book; + } + } + + public static class SaveEvent extends BookForm.BookFormEvent { + SaveEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class DeleteEvent extends BookForm.BookFormEvent { + DeleteEvent(BookForm source, Book book) { + super(source, book); + } + } + + public static class CloseEvent extends BookForm.BookFormEvent { + CloseEvent(BookForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(BookForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(BookForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(BookForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java new file mode 100644 index 0000000..9e04dc6 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookView.java @@ -0,0 +1,124 @@ +package de.thpeetz.kontor.bookshelf.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.BOOK_ROUTE, layout = MainLayout.class) +@PageTitle("Book | Bookshelf | Kontor") +public class BookView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Book.class); + TextField filterText = new TextField(); + @Getter + BookForm form; + BookshelfService service; + + public BookView(BookshelfService service) { + this.service = service; + addClassName("book-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("book-grid"); + grid.setSizeFull(); + grid.setColumns("title", "isbn", "publisher.name", "year"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editBook(event.getValue())); + } + + private void configureForm() { + form = new BookForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveBook); + form.addDeleteListener(this::deleteBook); + form.addCloseListener(e -> closeEditor()); + } + + private void saveBook(BookForm.SaveEvent event) { + service.saveBook(event.getBook()); + updateList(); + closeEditor(); + } + + private void deleteBook(BookForm.DeleteEvent event) { + service.deleteBook(event.getBook()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by title or isbn..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addBookButton = new Button("Add book"); + addBookButton.addClickListener(click -> addBook()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addBookButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editBook(Book book) { + if (book == null) { + closeEditor(); + } else { + form.setBook(book); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setBook(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addBook() { + grid.asSingleSelect().clear(); + editBook(new Book()); + } + + public void updateList() { + grid.setItems(service.findAllBooks(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java new file mode 100644 index 0000000..6105a47 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfLayout.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; + +public class BookshelfLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public BookshelfLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(BookshelfConstants.BOOKSHELF); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(BookshelfConstants.getBookLink(), BookshelfConstants.getArticleLink(), + BookshelfConstants.getPublisherLink(), BookshelfConstants.getAuthorLink()); + return navigation; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java new file mode 100644 index 0000000..de5f9b5 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/BookshelfPublisherView.java @@ -0,0 +1,131 @@ +package de.thpeetz.kontor.bookshelf.views; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.SeparateMainLayout; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import de.thpeetz.kontor.bookshelf.services.BookshelfService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = BookshelfConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Bookshelf | Kontor") +public class BookshelfPublisherView extends VerticalLayout { + + Grid grid = new Grid<>(BookshelfPublisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + BookshelfService service; + + public BookshelfPublisherView(BookshelfService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(BookshelfPublisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new BookshelfPublisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java new file mode 100644 index 0000000..7b48122 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/bookshelf/views/PublisherForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.bookshelf.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; + +public class PublisherForm extends FormLayout { + private TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder(BookshelfPublisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public TextField getName() { + return name; + } + + public void setName(TextField name) { + this.name = name; + } + + public void setPublisher(BookshelfPublisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private BookshelfPublisher publisher; + + protected PublisherFormEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public BookshelfPublisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, BookshelfPublisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java new file mode 100644 index 0000000..c89f7b1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/ComicConstants.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.comics; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.comics.views.ArtistView; +import de.thpeetz.kontor.comics.views.ComicView; +import de.thpeetz.kontor.comics.views.ComicWorkView; +import de.thpeetz.kontor.comics.views.IssueView; +import de.thpeetz.kontor.comics.views.PublisherView; +import de.thpeetz.kontor.comics.views.StoryArcView; +import de.thpeetz.kontor.comics.views.TradePaperbackView; +import de.thpeetz.kontor.comics.views.VolumeView; +import de.thpeetz.kontor.comics.views.WorktypeView; + +/** + * The {@code ComicConstants} class contains constant values related to comics. + */ +public class ComicConstants { + + public static final String COMICS = "Comics"; + public static final String COMICS_ROUTE = "comics/comic"; + public static final String PUBLISHER = "Publisher"; + public static final String PUBLISHER_ROUTE = "comics/publisher"; + public static final String COMICWORK = "ComicWork"; + public static final String COMICWORK_ROUTE = "comics/comicwork"; + public static final String WORKTYPE = "Worktype"; + public static final String WORKTYPE_ROUTE = "comics/worktype"; + public static final String ARTIST = "Artist"; + public static final String ARTIST_ROUTE = "comics/artist"; + public static final String TPB = "TradePaperback"; + public static final String TPB_ROUTE = "comics/tradepaperback"; + public static final String STORYARC = "Story Arc"; + public static final String STORYARC_ROTE = "comics/storyarc"; + public static final String ISSUE = "Issue"; + public static final String ISSUE_ROUTE = "comics/issue"; + public static final String VOLUME = "Volume"; + public static final String VOLUME_ROUTE = "comics/volume"; + + public static RouterLink getArtistLink() { + return new RouterLink(ARTIST, ArtistView.class); + } + + public static RouterLink getComicLink() { + return new RouterLink(COMICS, ComicView.class); + } + + public static RouterLink getPublisherLink() { + return new RouterLink(PUBLISHER, PublisherView.class); + } + + public static RouterLink getComicWorkLink() { + return new RouterLink(COMICWORK, ComicWorkView.class); + } + + public static RouterLink getIssueLink() { + return new RouterLink(ISSUE, IssueView.class); + } + + public static RouterLink getTradePaperbackLink() { + return new RouterLink(TPB, TradePaperbackView.class); + } + + public static Component getStoryArcLink() { + return new RouterLink(STORYARC, StoryArcView.class); + } + + public static RouterLink getWorktypeLink() { + return new RouterLink(WORKTYPE, WorktypeView.class); + } + + public static RouterLink getVolumeLink() { + return new RouterLink(VOLUME, VolumeView.class); + } + + public static SideNavItem getComicsNavigation() { + SideNavItem comics = new SideNavItem(COMICS, COMICS_ROUTE, VaadinIcon.RECORDS.create()); + comics.addItem(new SideNavItem(ARTIST, ArtistView.class)); + comics.addItem(new SideNavItem(COMICS, ComicView.class)); + comics.addItem(new SideNavItem(PUBLISHER, PublisherView.class)); + comics.addItem(new SideNavItem(ISSUE, IssueView.class)); + comics.addItem(new SideNavItem(TPB, TradePaperbackView.class)); + comics.addItem(new SideNavItem(STORYARC, StoryArcView.class)); + comics.addItem(new SideNavItem(VOLUME, VolumeView.class)); + return comics; + } + + private ComicConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java new file mode 100644 index 0000000..52674fd --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/SetupModuleComics.java @@ -0,0 +1,1198 @@ +package de.thpeetz.kontor.comics; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleComics implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private PublisherRepository publisherRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupModuleComics already executed, skipping"); + return; + } + if (!moduleService.importData(ComicConstants.COMICS)) { + log.info("Module comics should not setup data"); + return; + } + log.info("Set up Comics data"); + Publisher marvel = createPublisherIfNotFound("Marvel"); + Publisher alias = createPublisherIfNotFound("Alias"); + Publisher crossgen = createPublisherIfNotFound("Crossgen"); + Publisher image = createPublisherIfNotFound("Image"); + Publisher ddp = createPublisherIfNotFound("Devils Due Publishing"); + Publisher aspen = createPublisherIfNotFound("Aspen"); + Publisher bongo = createPublisherIfNotFound("Bongo Comics"); + Publisher kandora = createPublisherIfNotFound("Kandora"); + Publisher dc = createPublisherIfNotFound("DC"); + Publisher marvelknights = createPublisherIfNotFound("Marvel Knights"); + Publisher wildstorm = createPublisherIfNotFound("WildStorm"); + Publisher cliffhanger = createPublisherIfNotFound("Cliffhanger"); + Publisher darkhorse = createPublisherIfNotFound("Dark Horse Comics"); + Publisher broadsword = createPublisherIfNotFound("Broadsword"); + Publisher dynamite = createPublisherIfNotFound("Dynamite Entertainment"); + Publisher redeagle = createPublisherIfNotFound("Red Eagle Entertainment"); + Publisher topcow = createPublisherIfNotFound("Top Cow Productions"); + Publisher pulpfiction = createPublisherIfNotFound("Pulp Fiction"); + Artist michaelturner = createArtistIfNotFound("Turner, Michael"); + createArtistIfNotFound("Marz, Ron"); + createArtistIfNotFound("Whedon, Joss"); + createArtistIfNotFound("Land, Greg"); + Artist brianbendis = createArtistIfNotFound("Bendis, Brian Michael"); + Worktype writer = createWorktypeIfNotFound("Writer"); + createWorktypeIfNotFound("Penciler"); + createWorktypeIfNotFound("Inker"); + Comic comic1602 = createComicIfNotFound(marvel, "1602", false, false); + createComicIfNotFound(alias, "10th Muse", false, false); + Comic abadazad = createComicIfNotFound(crossgen, "Abadazad", false, false); + Comic amazingfantasy = createComicIfNotFound(marvel, "Amazing Fantasy", false, false); + Comic amazingspiderman = createComicIfNotFound(marvel, "Amazing Spider-Man", false, false); + Comic arana = createComicIfNotFound(marvel, "Arana", false, false); + Comic aria = createComicIfNotFound(image, "Aria", false, false); + createComicIfNotFound(ddp, "Army of Darkness", false, false); + Comic aspenComic = createComicIfNotFound(aspen, "Aspen", false, false); + Comic astonishingxmen = createComicIfNotFound(marvel, "Astonishing X-Men", false, false); + Comic athena = createComicIfNotFound(image, "Athena Inc. The Beginning", false, false); + Comic athenamanhunter = createComicIfNotFound(image, "Athena Inc. The Manhunter Project", false, false); + Comic barbarossa = createComicIfNotFound(kandora, "Barbarossa & The Lost Corsairs", false, false); + Comic bartsimpson = createComicIfNotFound(bongo, "Bart Simpson", false, false); + Comic bartsimpsontree = createComicIfNotFound(bongo, "Bart Simpsons Treehouse of Horror", false, false); + Comic battlepope = createComicIfNotFound(image, "Battle Pope", false, false); + Comic birdsofprey = createComicIfNotFound(dc, "Birds of Prey", false, false); + Comic blackwidow = createComicIfNotFound(marvelknights, "Black Widow", false, false); + Comic blackwidow2 = createComicIfNotFound(marvelknights, "Black Widow 2", false, false); + createComicIfNotFound(image, "Bluntman and Chronic", false, false); + Comic brath = createComicIfNotFound(crossgen, "Brath", false, false); + Comic catwomanrome = createComicIfNotFound(dc, "Catwoman When In Rome", false, false); + Comic crimson = createComicIfNotFound(wildstorm, "Crimson", false, false); + Comic crossgencomic = createComicIfNotFound(crossgen, "Crossgen", false, false); + Comic dangergirl = createComicIfNotFound(cliffhanger, "Danger Girl", false, false); + Comic dangergirlbackinblack = createComicIfNotFound(wildstorm, "Danger Girl Back in Black", false, false); + Comic daringescapes = createComicIfNotFound(image, "Daring Escapes", false, true); + Comic darknesssuperman = createComicIfNotFound(image, "Darkness / Superman", false, false); + Comic darknesstombraider = createComicIfNotFound(image, "Darkness / Tomb Raider", false, false); + Comic darknessvampirella = createComicIfNotFound(image, "Darkness / Vampirella", false, false); + Comic darknessblacksails = createComicIfNotFound(image, "Darkness Black Sails", false, false); + Comic darknessvol2 = createComicIfNotFound(image, "Darkness Vol. 2", false, false); + Comic districtx = createComicIfNotFound(marvel, "District X", false, false); + Comic dragonlance = createComicIfNotFound(ddp, "Dragonlance: Chronicles", false, false); + Comic dreampolice = createComicIfNotFound(marvel, "Dream Police", false, false); + Comic elcazador = createComicIfNotFound(crossgen, "El Cazador", false, false); + Comic elcazadortom = createComicIfNotFound(crossgen, "El Cazador The Bloody Ballad of Blackjack Tom", false, + false); + Comic elsinore = createComicIfNotFound(alias, "Elsinore", false, false); + Comic emmafrost = createComicIfNotFound(marvel, "Emma Frost", false, false); + Comic excalibur = createComicIfNotFound(marvel, "Excalibur", false, false); + Comic fathom = createComicIfNotFound(aspen, "Fathom", false, false); + Comic fathombeginnings = createComicIfNotFound(aspen, "Fathom Beginnings", false, false); + Comic fathomcannon = createComicIfNotFound(aspen, "Fathom Cannon Hawke", false, false); + Comic fathomcannonprelude = createComicIfNotFound(aspen, "Fathom Cannon Hawke Prelude", false, false); + Comic fathomdawnofwar = createComicIfNotFound(aspen, "Fathom Dawn of War", false, false); + Comic fathomprelude = createComicIfNotFound(aspen, "Fathom Prelude", false, false); + Comic fathomswimsuit = createComicIfNotFound(aspen, "Fathom Swimsuit Special", false, false); + Comic fathomswimsuit2000 = createComicIfNotFound(aspen, "Fathom Swimsuit Special 2000", false, false); + Comic fathomvol2 = createComicIfNotFound(aspen, "Fathom Vol. 2", false, false); + Comic fathomkillian = createComicIfNotFound(aspen, "Fathom: Killians Tide", false, false); + Comic flakriot = createComicIfNotFound(image, "Flak Riot", false, false); + Comic freshmen = createComicIfNotFound(image, "Freshmen", false, false); + Comic friendlyneighborspider = createComicIfNotFound(marvel, "Friendly Neighborhood Spider-Man", false, false); + Comic futurama = createComicIfNotFound(bongo, "Futurama", false, false); + Comic futuramaco2 = createComicIfNotFound(bongo, "Futurama Simpsons Crossover Crisis Part 2", false, false); + Comic ghostrider = createComicIfNotFound(marvelknights, "Ghostrider", false, false); + Comic gift = createComicIfNotFound(image, "Gift", false, false); + Comic hackslashtoys = createComicIfNotFound(ddp, "Hack Slash Land of Lost Toys", false, false); + Comic hackslashgirlsgonedead = createComicIfNotFound(ddp, "Hack/Slash: Girls Gone Dead", false, false); + Comic harryjohnson = createComicIfNotFound(pulpfiction, "Harry Johnson", false, false); + Comic hellcop = createComicIfNotFound(image, "Hellcop", false, false); + Comic holidayspecial2004 = createComicIfNotFound(marvel, "Holiday Special 2004", false, false); + Comic housem = createComicIfNotFound(marvel, "House of M", false, false); + Comic hunterkiller = createComicIfNotFound(image, "Hunter-Killer", false, false); + Comic hunterkillerdossier = createComicIfNotFound(image, "Hunter-Killer Dossier", false, false); + Comic ironghost = createComicIfNotFound(image, "Iron Ghost", false, false); + Comic judge = createComicIfNotFound(image, "J.U.D.G.E.: Secret Rage", false, false); + Comic kisskissbangbang = createComicIfNotFound(crossgen, "Kiss Kiss Bang Bang", false, false); + Comic legendofisis = createComicIfNotFound(alias, "Legend of Isis", false, false); + Comic loki = createComicIfNotFound(marvel, "Loki", false, false); + Comic lullaby = createComicIfNotFound(image, "Lullaby", false, false); + Comic magdalenavampirella2 = createComicIfNotFound(topcow, "Magdalena / Vampirella 2", false, false); + Comic marvelknightspider = createComicIfNotFound(marvelknights, "Marvel Knights Spider-Man", false, false); + Comic marville = createComicIfNotFound(marvel, "Marville", false, false); + Comic maryjane = createComicIfNotFound(marvel, "Mary Jane", false, false); + Comic maryjanehome = createComicIfNotFound(marvel, "Mary Jane Homecoming", false, false); + Comic megacity = createComicIfNotFound(ddp, "Megacity 909", false, false); + Comic meridian = createComicIfNotFound(crossgen, "Meridian", false, false); + Comic midnightnation = createComicIfNotFound(image, "Midnight Nation", false, true); + Comic monsterwar = createComicIfNotFound(image, "Monster War", false, false); + Comic monsterwar2005 = createComicIfNotFound(image, "Monster War 2005", false, false); + Comic mystic = createComicIfNotFound(crossgen, "Mystic", false, false); + Comic mystique = createComicIfNotFound(marvel, "Mystique", false, false); + Comic necromancer = createComicIfNotFound(image, "Necromancer", false, false); + Comic negationwar = createComicIfNotFound(crossgen, "Negation War", false, false); + Comic newavengers = createComicIfNotFound(marvel, "New Avengers", false, false); + Comic newmutants = createComicIfNotFound(marvel, "New Mutants", false, false); + Comic newxmen = createComicIfNotFound(marvel, "New X-Men", false, false); + Comic newxmenacademy = createComicIfNotFound(marvel, "New X-Men Academy X", false, false); + Comic newxmenhellions = createComicIfNotFound(marvel, "New X-Men Hellions", false, false); + Comic nightcrawler = createComicIfNotFound(marvel, "Nightcrawler", false, false); + Comic ororo = createComicIfNotFound(marvel, "Ororo: Before the Storm", false, false); + Comic radix = createComicIfNotFound(image, "Radix", false, false); + Comic redsonja = createComicIfNotFound(dynamite, "Red Sonja", false, false); + Comic revelations = createComicIfNotFound(darkhorse, "Revelations", false, false); + Comic rogue = createComicIfNotFound(marvel, "Rogue", false, false); + Comic ruse = createComicIfNotFound(crossgen, "Ruse", false, false); + Comic samurai = createComicIfNotFound(darkhorse, "Samurai: Heaven & Earth", false, false); + Comic scion = createComicIfNotFound(crossgen, "Scion", false, false); + Comic shanna = createComicIfNotFound(marvelknights, "Shanna, The She-Devil", false, false); + Comic shehulk = createComicIfNotFound(marvel, "She-Hulk", false, false); + Comic shehulk2 = createComicIfNotFound(marvel, "She-Hulk 2", false, false); + Comic shijunen = createComicIfNotFound(darkhorse, "Shi Ju-Nen", false, false); + Comic shrek = createComicIfNotFound(darkhorse, "Shrek", false, false); + Comic simpsons = createComicIfNotFound(bongo, "Simpsons", false, false); + Comic sojourn = createComicIfNotFound(crossgen, "Sojourn", false, false); + Comic solus = createComicIfNotFound(crossgen, "Solus", false, false); + Comic soulfire = createComicIfNotFound(aspen, "Soulfire", false, false); + Comic soulfirelight = createComicIfNotFound(aspen, "Soulfire Dying of the Light", false, false); + Comic spectacularspiderman = createComicIfNotFound(marvel, "Spectacular Spider-Man", false, false); + Comic spellbinders = createComicIfNotFound(marvel, "Spellbinders", false, false); + Comic spidermanindia = createComicIfNotFound(marvel, "Spider-Man India", false, false); + Comic spidermanlovesmary = createComicIfNotFound(marvel, "Spider-Man loves Mary Jane", false, false); + Comic spidermanteam = createComicIfNotFound(marvel, "Spider-Man Team Up", false, false); + Comic spidermanbreakout = createComicIfNotFound(marvel, "Spider-Man: Breakout", false, false); + Comic spidermanhousem = createComicIfNotFound(marvel, "Spider-Man: House of M", false, false); + Comic starwars = createComicIfNotFound(darkhorse, "Star Wars", false, false); + Comic stardustkid = createComicIfNotFound(image, "Stardust Kid", false, false); + Comic strange = createComicIfNotFound(marvel, "Strange", false, false); + Comic supergirl = createComicIfNotFound(dc, "Supergirl", false, false); + Comic superman = createComicIfNotFound(dc, "Superman", false, false); + Comic supermanbatman = createComicIfNotFound(dc, "Superman/Batman", false, false); + Comic tarotblackrose = createComicIfNotFound(broadsword, "Tarot: Witch of the Black Rose", false, false); + Comic artgreghorn = createComicIfNotFound(image, "The Art of Greg Horn", false, false); + Comic devilskeeper = createComicIfNotFound(alias, "The Devil´s Keeper", false, false); + Comic thetenth = createComicIfNotFound(image, "The Tenth", false, false); + Comic tombdracula = createComicIfNotFound(marvel, "The Tomb of Dracula", false, false); + Comic robertjordanwheeloftime = createComicIfNotFound(redeagle, "Robert Jordan´s The Wheel of Time: New Spring", + false, false); + Comic thorsonasgard = createComicIfNotFound(marvel, "Thor: Son of Asgard", false, false); + Comic tomstrong = createComicIfNotFound(wildstorm, "Tom Strong", false, false); + Comic tombraider = createComicIfNotFound(image, "Tomb Raider", false, false); + Comic tombraidergreatesttreasure = createComicIfNotFound(image, "Tomb Raider: The Greatest Treasure of All", + false, false); + Comic toxin = createComicIfNotFound(marvel, "Toxin", false, false); + Comic ultimatefantasticfour = createComicIfNotFound(marvel, "Ultimate Fantastic Four", false, false); + Comic ultimatespidermanannual = createComicIfNotFound(marvel, "Ultimate Spider-Man Annual", false, false); + Comic uncannyxmen = createComicIfNotFound(marvel, "Uncanny X-Men", false, false); + Comic vampirella = createComicIfNotFound(dynamite, "Vampirella", false, false); + Comic wildgirl = createComicIfNotFound(wildstorm, "Wild Girl", false, false); + Comic wildcats = createComicIfNotFound(wildstorm, "Wildcats: Nemesis", false, false); + Comic wildsiderz = createComicIfNotFound(wildstorm, "Wildsiderz", false, false); + Comic witchblade = createComicIfNotFound(image, "Witchblade", false, false); + Comic witchbladetombraider = createComicIfNotFound(image, "Witchblade / Tomb Raider", false, false); + Comic wolverine = createComicIfNotFound(marvel, "Wolverine: The End", false, false); + Comic woodboy = createComicIfNotFound(image, "Wood Boy", false, false); + Comic wraithborn = createComicIfNotFound(wildstorm, "Wraithborn", false, false); + Comic x23 = createComicIfNotFound(marvel, "X-23", false, false); + Comic xmenageofapocalypse = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse", false, false); + Comic xmenageofapocalypseoneshot = createComicIfNotFound(marvel, "X-Men: Age of Apocalypse One Shot", false, + false); + Comic xmenkittypryde = createComicIfNotFound(marvel, "X-Men: Kitty Pryde", false, false); + Comic phoenix = createComicIfNotFound(marvel, "X-Men: Phoenix - Endsong", false, false); + Comic xtremexmen = createComicIfNotFound(marvel, "X-treme X-Men", false, false); + Comic armydarknessreanim = createComicIfNotFound(dynamite, "Army of Darkness vs. Re-Animator", false, false); + Comic runaways = createComicIfNotFound(marvel, "Runaways", false, false); + Comic crux = createComicIfNotFound(crossgen, "Crux", false, false); + Comic ariasoulmarket = createComicIfNotFound(image, "Aria: The Soul Market", false, false); + Comic ariasummerspell = createComicIfNotFound(image, "Aria: Summer´s Spell", false, false); + Comic ariaenchantment = createComicIfNotFound(image, "Aria: The Uses of Enchantment", false, false); + Comic armydarknessashes = createComicIfNotFound(darkhorse, "Army of Darkness: Ashes 2 Ashes", false, false); + Comic armydarknessshop = createComicIfNotFound(darkhorse, "Army of Darkness: Shop Till You Drop Dead", false, + false); + createComicIfNotFound(marvel, "X-Men", false, false); + createComicIfNotFound(image, "Bomb Queen II: Queen of Hearts", false, false); + createComicIfNotFound(image, "Bomb Queen III: The Good, The Bad and The Lovely", false, false); + createComicIfNotFound(image, "Bomb Queen IV: Suicide Bomber", false, false); + createComicIfNotFound(wildstorm, "Gen13", false, false); + createComicIfNotFound(aspen, "Iron & The Maiden", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Rebellion", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Knights of the Old Republic", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Legacy", false, false); + createComicIfNotFound(darkhorse, "Star Wars: Dark Times", false, false); + createComicWorkIfNotFound(crossgencomic, michaelturner, writer); + createComicWorkIfNotFound(dangergirl, michaelturner, writer); + createComicWorkIfNotFound(ultimatefantasticfour, michaelturner, writer); + createComicWorkIfNotFound(ultimatespidermanannual, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, michaelturner, writer); + createComicWorkIfNotFound(starwars, michaelturner, writer); + createComicWorkIfNotFound(shehulk, michaelturner, writer); + createComicWorkIfNotFound(shehulk2, michaelturner, writer); + createComicWorkIfNotFound(scion, michaelturner, writer); + createComicWorkIfNotFound(newavengers, michaelturner, writer); + createComicWorkIfNotFound(newmutants, michaelturner, writer); + createComicWorkIfNotFound(midnightnation, michaelturner, writer); + createComicWorkIfNotFound(monsterwar, michaelturner, writer); + createComicWorkIfNotFound(monsterwar2005, michaelturner, writer); + createComicWorkIfNotFound(mystic, michaelturner, writer); + createComicWorkIfNotFound(holidayspecial2004, michaelturner, writer); + createComicWorkIfNotFound(hackslashgirlsgonedead, michaelturner, writer); + createComicWorkIfNotFound(uncannyxmen, brianbendis, writer); + createStoryArcIfNotFound("Higher Learning", emmafrost); + createStoryArcIfNotFound("Mind Games", emmafrost); + createStoryArcIfNotFound("Bloom", emmafrost); + createTradePaperbackIfNotFound("Vol. 1", midnightnation, 1, 12); + createTradePaperbackIfNotFound("From The Ashes", sojourn, 1, 6); + createTradePaperbackIfNotFound("The Dragons Tale", sojourn, 7, 12); + createTradePaperbackIfNotFound("The Warriors Tale", sojourn, 13, 18); + createTradePaperbackIfNotFound("The Thiefs Tale", sojourn, 19, 24); + createTradePaperbackIfNotFound("Vol. 1", runaways, 1, 18); + createTradePaperbackIfNotFound("Choices", gift, 1, 5); + createTradePaperbackIfNotFound("Atlantis Rising", crux, 1, 6); + createTradePaperbackIfNotFound("Test Of Time", crux, 7, 12); + createTradePaperbackIfNotFound("Strangers in Atlantis", crux, 13, 18); + createTradePaperbackIfNotFound("Flying Solo", meridian, 1, 7); + createTradePaperbackIfNotFound("Going To Ground", meridian, 8, 14); + createTradePaperbackIfNotFound("Taking The Skies", meridian, 15, 20); + createTradePaperbackIfNotFound("Coming Home", meridian, 21, 26); + createTradePaperbackIfNotFound("Rite of Passage", mystic, 1, 7); + createTradePaperbackIfNotFound("The Demon Queen", mystic, 8, 14); + createTradePaperbackIfNotFound("Siege of Scales", mystic, 15, 20); + createTradePaperbackIfNotFound("Out All Night", mystic, 21, 26); + createTradePaperbackIfNotFound("Single Green Female", shehulk, 1, 6); + createTradePaperbackIfNotFound("Superhuman Law", shehulk, 7, 12); + createTradePaperbackIfNotFound("Conflict of Conscience", scion, 1, 7); + createTradePaperbackIfNotFound("Blood For Blood", scion, 8, 14); + createTradePaperbackIfNotFound("Divided Loyalties", scion, 15, 21); + createTradePaperbackIfNotFound("Sanctuary", scion, 22, 27); + createTradePaperbackIfNotFound("The End Of History", uncannyxmen, 444, 449); + createTradePaperbackIfNotFound("Public Enemies", supermanbatman, 1, 6); + createTradePaperbackIfNotFound("Loyalty And Loss", crimson, 1, 6); + createTradePaperbackIfNotFound("Heaven & Earth", crimson, 7, 12); + createTradePaperbackIfNotFound("Earth Angel", crimson, 13, 18); + createTradePaperbackIfNotFound("Redemption", crimson, 19, 24); + createTradePaperbackIfNotFound("1602", comic1602, 1, 8); + createTradePaperbackIfNotFound("Coming Home", amazingspiderman, 30, 35); + createTradePaperbackIfNotFound("Revelations", amazingspiderman, 36, 39); + createTradePaperbackIfNotFound("Until the Stars Turn Cold", amazingspiderman, 40, 45); + createTradePaperbackIfNotFound("The Life & Death of Spiders", amazingspiderman, 46, 50); + createTradePaperbackIfNotFound("Unintended Consequences", amazingspiderman, 51, 56); + createTradePaperbackIfNotFound("Happy Birthday", amazingspiderman, 500, 502); + createTradePaperbackIfNotFound("Sonderband 1", dangergirl, 1, 2); + createTradePaperbackIfNotFound("Of Like Minds", birdsofprey, 56, 61); + createTradePaperbackIfNotFound("Sensei & Student", birdsofprey, 62, 68); + createIssueIfNotFound("1", phoenix, false, false); + createIssueIfNotFound("2", phoenix, false, false); + createIssueIfNotFound("3", phoenix, false, false); + createIssueIfNotFound("4", phoenix, false, false); + createIssueIfNotFound("5", phoenix, false, false); + createIssueIfNotFound("1", midnightnation, true, false); + createIssueIfNotFound("2", midnightnation, true, false); + createIssueIfNotFound("3", midnightnation, true, false); + createIssueIfNotFound("4", midnightnation, true, false); + createIssueIfNotFound("5", midnightnation, true, false); + createIssueIfNotFound("6", midnightnation, true, false); + createIssueIfNotFound("7", midnightnation, true, false); + createIssueIfNotFound("8", midnightnation, true, false); + createIssueIfNotFound("9", midnightnation, true, false); + createIssueIfNotFound("10", midnightnation, true, false); + createIssueIfNotFound("11", midnightnation, true, false); + createIssueIfNotFound("12", midnightnation, true, false); + createIssueIfNotFound("1", arana, false, false); + createIssueIfNotFound("2", arana, false, false); + createIssueIfNotFound("3", arana, false, false); + createIssueIfNotFound("4", arana, false, false); + createIssueIfNotFound("5", arana, false, false); + createIssueIfNotFound("6", arana, false, false); + createIssueIfNotFound("7", arana, false, false); + createIssueIfNotFound("8", arana, false, false); + createIssueIfNotFound("9", arana, false, false); + createIssueIfNotFound("10", arana, false, false); + createIssueIfNotFound("11", arana, false, false); + createIssueIfNotFound("1", futurama, false, false); + createIssueIfNotFound("2", futurama, false, false); + createIssueIfNotFound("3", futurama, false, false); + createIssueIfNotFound("4", futurama, false, false); + createIssueIfNotFound("5", futurama, false, false); + createIssueIfNotFound("6", futurama, false, false); + createIssueIfNotFound("7", futurama, false, false); + createIssueIfNotFound("8", futurama, false, false); + createIssueIfNotFound("9", futurama, false, false); + createIssueIfNotFound("10", futurama, false, false); + createIssueIfNotFound("11", futurama, false, false); + createIssueIfNotFound("12", futurama, false, false); + createIssueIfNotFound("13", futurama, false, false); + createIssueIfNotFound("14", futurama, false, false); + createIssueIfNotFound("15", futurama, false, false); + createIssueIfNotFound("16", futurama, false, false); + createIssueIfNotFound("17", futurama, false, false); + createIssueIfNotFound("18", futurama, false, false); + createIssueIfNotFound("19", futurama, false, false); + createIssueIfNotFound("20", futurama, false, false); + createIssueIfNotFound("21", futurama, false, false); + createIssueIfNotFound("22", futurama, false, false); + createIssueIfNotFound("1", futuramaco2, false, false); + createIssueIfNotFound("2", futuramaco2, false, false); + createIssueIfNotFound("1", battlepope, false, false); + createIssueIfNotFound("2", battlepope, false, false); + createIssueIfNotFound("3", battlepope, false, false); + createIssueIfNotFound("11", bartsimpsontree, false, false); + createIssueIfNotFound("20", bartsimpson, false, false); + createIssueIfNotFound("21", bartsimpson, false, false); + createIssueIfNotFound("22", bartsimpson, false, false); + createIssueIfNotFound("23", bartsimpson, false, false); + createIssueIfNotFound("24", bartsimpson, false, false); + createIssueIfNotFound("25", bartsimpson, false, false); + createIssueIfNotFound("1", athena, false, false); + createIssueIfNotFound("1", athenamanhunter, false, false); + createIssueIfNotFound("2", athenamanhunter, false, false); + createIssueIfNotFound("3", athenamanhunter, false, false); + createIssueIfNotFound("4", athenamanhunter, false, false); + createIssueIfNotFound("5", athenamanhunter, false, false); + createIssueIfNotFound("6", athenamanhunter, false, false); + createIssueIfNotFound("1", blackwidow, false, false); + createIssueIfNotFound("2", blackwidow, false, false); + createIssueIfNotFound("3", blackwidow, false, false); + createIssueIfNotFound("4", blackwidow, false, false); + createIssueIfNotFound("5", blackwidow, false, false); + createIssueIfNotFound("6", blackwidow, false, false); + createIssueIfNotFound("1", blackwidow2, false, false); + createIssueIfNotFound("2", blackwidow2, false, false); + createIssueIfNotFound("3", blackwidow2, false, false); + createIssueIfNotFound("4", blackwidow2, false, false); + createIssueIfNotFound("5", blackwidow2, false, false); + createIssueIfNotFound("1", ruse, false, false); + createIssueIfNotFound("2", ruse, false, false); + createIssueIfNotFound("3", ruse, false, false); + createIssueIfNotFound("4", ruse, false, false); + createIssueIfNotFound("5", ruse, false, false); + createIssueIfNotFound("6", ruse, false, false); + createIssueIfNotFound("7", ruse, false, false); + createIssueIfNotFound("8", ruse, false, false); + createIssueIfNotFound("9", ruse, false, false); + createIssueIfNotFound("10", ruse, false, false); + createIssueIfNotFound("11", ruse, false, false); + createIssueIfNotFound("12", ruse, false, false); + createIssueIfNotFound("13", ruse, false, false); + createIssueIfNotFound("14", ruse, false, false); + createIssueIfNotFound("15", ruse, false, false); + createIssueIfNotFound("16", ruse, false, false); + createIssueIfNotFound("17", ruse, false, false); + createIssueIfNotFound("18", ruse, false, false); + createIssueIfNotFound("19", ruse, false, false); + createIssueIfNotFound("20", ruse, false, false); + createIssueIfNotFound("21", ruse, false, false); + createIssueIfNotFound("22", ruse, false, false); + createIssueIfNotFound("23", ruse, false, false); + createIssueIfNotFound("24", ruse, false, false); + createIssueIfNotFound("25", ruse, false, false); + createIssueIfNotFound("26", ruse, false, false); + createIssueIfNotFound("1", samurai, false, false); + createIssueIfNotFound("2", samurai, false, false); + createIssueIfNotFound("3", samurai, false, false); + createIssueIfNotFound("4", samurai, false, false); + createIssueIfNotFound("1", amazingfantasy, false, false); + createIssueIfNotFound("2", amazingfantasy, false, false); + createIssueIfNotFound("3", amazingfantasy, false, false); + createIssueIfNotFound("4", amazingfantasy, false, false); + createIssueIfNotFound("5", amazingfantasy, false, false); + createIssueIfNotFound("6", amazingfantasy, false, false); + createIssueIfNotFound("7", amazingfantasy, false, false); + createIssueIfNotFound("8", amazingfantasy, false, false); + createIssueIfNotFound("9", amazingfantasy, false, false); + createIssueIfNotFound("10", amazingfantasy, false, false); + createIssueIfNotFound("11", amazingfantasy, false, false); + createIssueIfNotFound("12", amazingfantasy, false, false); + createIssueIfNotFound("13", amazingfantasy, false, false); + createIssueIfNotFound("14", amazingfantasy, false, false); + createIssueIfNotFound("15", amazingfantasy, false, false); + createIssueIfNotFound("1", excalibur, false, false); + createIssueIfNotFound("2", excalibur, false, false); + createIssueIfNotFound("3", excalibur, false, false); + createIssueIfNotFound("4", excalibur, false, false); + createIssueIfNotFound("5", excalibur, false, false); + createIssueIfNotFound("6", excalibur, false, false); + createIssueIfNotFound("7", excalibur, false, false); + createIssueIfNotFound("8", excalibur, false, false); + createIssueIfNotFound("9", excalibur, false, false); + createIssueIfNotFound("10", excalibur, false, false); + createIssueIfNotFound("11", excalibur, false, false); + createIssueIfNotFound("12", excalibur, false, false); + createIssueIfNotFound("1", emmafrost, false, false); + createIssueIfNotFound("2", emmafrost, false, false); + createIssueIfNotFound("3", emmafrost, false, false); + createIssueIfNotFound("4", emmafrost, false, false); + createIssueIfNotFound("5", emmafrost, false, false); + createIssueIfNotFound("6", emmafrost, false, false); + createIssueIfNotFound("7", emmafrost, false, false); + createIssueIfNotFound("8", emmafrost, false, false); + createIssueIfNotFound("9", emmafrost, false, false); + createIssueIfNotFound("10", emmafrost, false, false); + createIssueIfNotFound("11", emmafrost, false, false); + createIssueIfNotFound("12", emmafrost, false, false); + createIssueIfNotFound("13", emmafrost, false, false); + createIssueIfNotFound("14", emmafrost, false, false); + createIssueIfNotFound("15", emmafrost, false, false); + createIssueIfNotFound("16", emmafrost, false, false); + createIssueIfNotFound("17", emmafrost, false, false); + createIssueIfNotFound("18", emmafrost, false, false); + createIssueIfNotFound("1", catwomanrome, false, false); + createIssueIfNotFound("2", catwomanrome, false, false); + createIssueIfNotFound("3", catwomanrome, false, false); + createIssueIfNotFound("4", catwomanrome, false, false); + createIssueIfNotFound("5", catwomanrome, false, false); + createIssueIfNotFound("6", catwomanrome, false, false); + createIssueIfNotFound("1", districtx, false, false); + createIssueIfNotFound("2", districtx, false, false); + createIssueIfNotFound("3", districtx, false, false); + createIssueIfNotFound("4", districtx, false, false); + createIssueIfNotFound("5", districtx, false, false); + createIssueIfNotFound("6", districtx, false, false); + createIssueIfNotFound("7", districtx, false, false); + createIssueIfNotFound("8", districtx, false, false); + createIssueIfNotFound("9", districtx, false, false); + createIssueIfNotFound("10", districtx, false, false); + createIssueIfNotFound("11", districtx, false, false); + createIssueIfNotFound("12", districtx, false, false); + createIssueIfNotFound("13", districtx, false, false); + createIssueIfNotFound("14", districtx, false, false); + createIssueIfNotFound("1", elcazador, false, false); + createIssueIfNotFound("2", elcazador, false, false); + createIssueIfNotFound("3", elcazador, false, false); + createIssueIfNotFound("4", elcazador, false, false); + createIssueIfNotFound("5", elcazador, false, false); + createIssueIfNotFound("6", elcazador, false, false); + createIssueIfNotFound("1", elcazadortom, false, false); + createIssueIfNotFound("1", elsinore, false, false); + createIssueIfNotFound("1", dreampolice, false, false); + createIssueIfNotFound("1", dragonlance, false, false); + createIssueIfNotFound("2", dragonlance, false, false); + createIssueIfNotFound("1", ghostrider, false, false); + createIssueIfNotFound("2", ghostrider, false, false); + createIssueIfNotFound("3", ghostrider, false, false); + createIssueIfNotFound("4", ghostrider, false, false); + createIssueIfNotFound("5", ghostrider, false, false); + createIssueIfNotFound("6", ghostrider, false, false); + createIssueIfNotFound("1", fathomkillian, false, false); + createIssueIfNotFound("2", fathomkillian, false, false); + createIssueIfNotFound("3", fathomkillian, false, false); + createIssueIfNotFound("4", fathomkillian, false, false); + createIssueIfNotFound("1", maryjane, false, false); + createIssueIfNotFound("2", maryjane, false, false); + createIssueIfNotFound("3", maryjane, false, false); + createIssueIfNotFound("4", maryjane, false, false); + createIssueIfNotFound("1", maryjanehome, false, false); + createIssueIfNotFound("2", maryjanehome, false, false); + createIssueIfNotFound("3", maryjanehome, false, false); + createIssueIfNotFound("4", maryjanehome, false, false); + createIssueIfNotFound("1", marville, false, false); + createIssueIfNotFound("2", marville, false, false); + createIssueIfNotFound("3", marville, false, false); + createIssueIfNotFound("4", marville, false, false); + createIssueIfNotFound("5", marville, false, false); + createIssueIfNotFound("6", marville, false, false); + createIssueIfNotFound("7", marville, false, false); + createIssueIfNotFound("1", megacity, false, false); + createIssueIfNotFound("2", megacity, false, false); + createIssueIfNotFound("3", megacity, false, false); + createIssueIfNotFound("4", megacity, false, false); + createIssueIfNotFound("5", megacity, false, false); + createIssueIfNotFound("6", megacity, false, false); + createIssueIfNotFound("7", megacity, false, false); + createIssueIfNotFound("8", megacity, false, false); + createIssueIfNotFound("1", nightcrawler, false, false); + createIssueIfNotFound("2", nightcrawler, false, false); + createIssueIfNotFound("3", nightcrawler, false, false); + createIssueIfNotFound("4", nightcrawler, false, false); + createIssueIfNotFound("5", nightcrawler, false, false); + createIssueIfNotFound("6", nightcrawler, false, false); + createIssueIfNotFound("7", nightcrawler, false, false); + createIssueIfNotFound("8", nightcrawler, false, false); + createIssueIfNotFound("9", nightcrawler, false, false); + createIssueIfNotFound("10", nightcrawler, false, false); + createIssueIfNotFound("11", nightcrawler, false, false); + createIssueIfNotFound("12", nightcrawler, false, false); + createIssueIfNotFound("1", ororo, false, false); + createIssueIfNotFound("1", radix, false, false); + createIssueIfNotFound("2", radix, false, false); + createIssueIfNotFound("3", radix, false, false); + createIssueIfNotFound("1", rogue, false, false); + createIssueIfNotFound("2", rogue, false, false); + createIssueIfNotFound("3", rogue, false, false); + createIssueIfNotFound("4", rogue, false, false); + createIssueIfNotFound("5", rogue, false, false); + createIssueIfNotFound("6", rogue, false, false); + createIssueIfNotFound("7", rogue, false, false); + createIssueIfNotFound("8", rogue, false, false); + createIssueIfNotFound("9", rogue, false, false); + createIssueIfNotFound("10", rogue, false, false); + createIssueIfNotFound("11", rogue, false, false); + createIssueIfNotFound("12", rogue, false, false); + createIssueIfNotFound("1", shijunen, false, false); + createIssueIfNotFound("2", shijunen, false, false); + createIssueIfNotFound("3", shijunen, false, false); + createIssueIfNotFound("4", shijunen, false, false); + createIssueIfNotFound("1", solus, false, false); + createIssueIfNotFound("2", solus, false, false); + createIssueIfNotFound("3", solus, false, false); + createIssueIfNotFound("4", solus, false, false); + createIssueIfNotFound("5", solus, false, false); + createIssueIfNotFound("6", solus, false, false); + createIssueIfNotFound("7", solus, false, false); + createIssueIfNotFound("8", solus, false, false); + createIssueIfNotFound("1", toxin, false, false); + createIssueIfNotFound("2", toxin, false, false); + createIssueIfNotFound("3", toxin, false, false); + createIssueIfNotFound("4", toxin, false, false); + createIssueIfNotFound("5", toxin, false, false); + createIssueIfNotFound("6", toxin, false, false); + createIssueIfNotFound("1", wildgirl, false, false); + createIssueIfNotFound("2", wildgirl, false, false); + createIssueIfNotFound("3", wildgirl, false, false); + createIssueIfNotFound("4", wildgirl, false, false); + createIssueIfNotFound("5", wildgirl, false, false); + createIssueIfNotFound("6", wildgirl, false, false); + createIssueIfNotFound("1", wildcats, false, false); + createIssueIfNotFound("1", wildsiderz, false, false); + createIssueIfNotFound("19", vampirella, false, false); + createIssueIfNotFound("1", wolverine, false, false); + createIssueIfNotFound("2", wolverine, false, false); + createIssueIfNotFound("3", wolverine, false, false); + createIssueIfNotFound("4", wolverine, false, false); + createIssueIfNotFound("5", wolverine, false, false); + createIssueIfNotFound("6", wolverine, false, false); + createIssueIfNotFound("1", woodboy, false, false); + createIssueIfNotFound("1", wraithborn, false, false); + createIssueIfNotFound("2", wraithborn, false, false); + createIssueIfNotFound("3", wraithborn, false, false); + createIssueIfNotFound("1", x23, false, false); + createIssueIfNotFound("2", x23, false, false); + createIssueIfNotFound("3", x23, false, false); + createIssueIfNotFound("4", x23, false, false); + createIssueIfNotFound("5", x23, false, false); + createIssueIfNotFound("6", x23, false, false); + createIssueIfNotFound("46", xtremexmen, false, false); + createIssueIfNotFound("1", xmenkittypryde, false, false); + createIssueIfNotFound("2", xmenkittypryde, false, false); + createIssueIfNotFound("3", xmenkittypryde, false, false); + createIssueIfNotFound("4", xmenkittypryde, false, false); + createIssueIfNotFound("5", xmenkittypryde, false, false); + createIssueIfNotFound("1", witchbladetombraider, false, false); + createIssueIfNotFound("1", xmenageofapocalypseoneshot, false, false); + createIssueIfNotFound("1", xmenageofapocalypse, false, false); + createIssueIfNotFound("2", xmenageofapocalypse, false, false); + createIssueIfNotFound("3", xmenageofapocalypse, false, false); + createIssueIfNotFound("4", xmenageofapocalypse, false, false); + createIssueIfNotFound("5", xmenageofapocalypse, false, false); + createIssueIfNotFound("6", xmenageofapocalypse, false, false); + createIssueIfNotFound("1", thorsonasgard, false, false); + createIssueIfNotFound("2", thorsonasgard, false, false); + createIssueIfNotFound("3", thorsonasgard, false, false); + createIssueIfNotFound("4", thorsonasgard, false, false); + createIssueIfNotFound("5", thorsonasgard, false, false); + createIssueIfNotFound("6", thorsonasgard, false, false); + createIssueIfNotFound("7", thorsonasgard, false, false); + createIssueIfNotFound("8", thorsonasgard, false, false); + createIssueIfNotFound("9", thorsonasgard, false, false); + createIssueIfNotFound("10", thorsonasgard, false, false); + createIssueIfNotFound("11", thorsonasgard, false, false); + createIssueIfNotFound("12", thorsonasgard, false, false); + createIssueIfNotFound("1", strange, false, false); + createIssueIfNotFound("2", strange, false, false); + createIssueIfNotFound("3", strange, false, false); + createIssueIfNotFound("4", strange, false, false); + createIssueIfNotFound("5", strange, false, false); + createIssueIfNotFound("6", strange, false, false); + createIssueIfNotFound("0", supergirl, false, false); + createIssueIfNotFound("1", supergirl, false, false); + createIssueIfNotFound("2", supergirl, false, false); + createIssueIfNotFound("3", supergirl, false, false); + createIssueIfNotFound("4", supergirl, false, false); + createIssueIfNotFound("1", robertjordanwheeloftime, false, false); + createIssueIfNotFound("2", robertjordanwheeloftime, false, false); + createIssueIfNotFound("1", stardustkid, false, false); + createIssueIfNotFound("2", stardustkid, false, false); + createIssueIfNotFound("3", stardustkid, false, false); + createIssueIfNotFound("207", superman, false, false); + createIssueIfNotFound("208", superman, false, false); + createIssueIfNotFound("209", superman, false, false); + createIssueIfNotFound("210", superman, false, false); + createIssueIfNotFound("211", superman, false, false); + createIssueIfNotFound("212", superman, false, false); + createIssueIfNotFound("213", superman, false, false); + createIssueIfNotFound("214", superman, false, false); + createIssueIfNotFound("215", superman, false, false); + createIssueIfNotFound("17", supermanbatman, false, false); + createIssueIfNotFound("18", supermanbatman, false, false); + createIssueIfNotFound("19", supermanbatman, false, false); + createIssueIfNotFound("20", supermanbatman, false, false); + createIssueIfNotFound("21", supermanbatman, false, false); + createIssueIfNotFound("22", supermanbatman, false, false); + createIssueIfNotFound("1", tombraidergreatesttreasure, false, false); + createIssueIfNotFound("48", tombraider, false, false); + createIssueIfNotFound("49", tombraider, false, false); + createIssueIfNotFound("50", tombraider, false, false); + createIssueIfNotFound("1", tomstrong, false, false); + createIssueIfNotFound("1", tombdracula, false, false); + createIssueIfNotFound("1", thetenth, false, false); + createIssueIfNotFound("1", devilskeeper, false, false); + createIssueIfNotFound("1", artgreghorn, false, false); + createIssueIfNotFound("81", witchblade, false, false); + createIssueIfNotFound("82", witchblade, false, false); + createIssueIfNotFound("83", witchblade, false, false); + createIssueIfNotFound("84", witchblade, false, false); + createIssueIfNotFound("85", witchblade, false, false); + createIssueIfNotFound("86", witchblade, false, false); + createIssueIfNotFound("87", witchblade, false, false); + createIssueIfNotFound("88", witchblade, false, false); + createIssueIfNotFound("89", witchblade, false, false); + createIssueIfNotFound("90", witchblade, false, false); + createIssueIfNotFound("91", witchblade, false, false); + createIssueIfNotFound("92", witchblade, false, false); + createIssueIfNotFound("19", tarotblackrose, false, false); + createIssueIfNotFound("20", tarotblackrose, false, false); + createIssueIfNotFound("21", tarotblackrose, false, false); + createIssueIfNotFound("22", tarotblackrose, false, false); + createIssueIfNotFound("23", tarotblackrose, false, false); + createIssueIfNotFound("24", tarotblackrose, false, false); + createIssueIfNotFound("25", tarotblackrose, false, false); + createIssueIfNotFound("26", tarotblackrose, false, false); + createIssueIfNotFound("27", tarotblackrose, false, false); + createIssueIfNotFound("28", tarotblackrose, false, false); + createIssueIfNotFound("29", tarotblackrose, false, false); + createIssueIfNotFound("30", tarotblackrose, false, false); + createIssueIfNotFound("31", tarotblackrose, false, false); + createIssueIfNotFound("32", tarotblackrose, false, false); + createIssueIfNotFound("33", tarotblackrose, false, false); + createIssueIfNotFound("34", tarotblackrose, false, false); + createIssueIfNotFound("35", tarotblackrose, false, false); + createIssueIfNotFound("1", spectacularspiderman, false, false); + createIssueIfNotFound("2", spectacularspiderman, false, false); + createIssueIfNotFound("3", spectacularspiderman, false, false); + createIssueIfNotFound("4", spectacularspiderman, false, false); + createIssueIfNotFound("5", spectacularspiderman, false, false); + createIssueIfNotFound("6", spectacularspiderman, false, false); + createIssueIfNotFound("7", spectacularspiderman, false, false); + createIssueIfNotFound("8", spectacularspiderman, false, false); + createIssueIfNotFound("9", spectacularspiderman, false, false); + createIssueIfNotFound("10", spectacularspiderman, false, false); + createIssueIfNotFound("11", spectacularspiderman, false, false); + createIssueIfNotFound("12", spectacularspiderman, false, false); + createIssueIfNotFound("13", spectacularspiderman, false, false); + createIssueIfNotFound("14", spectacularspiderman, false, false); + createIssueIfNotFound("15", spectacularspiderman, false, false); + createIssueIfNotFound("16", spectacularspiderman, false, false); + createIssueIfNotFound("17", spectacularspiderman, false, false); + createIssueIfNotFound("18", spectacularspiderman, false, false); + createIssueIfNotFound("19", spectacularspiderman, false, false); + createIssueIfNotFound("20", spectacularspiderman, false, false); + createIssueIfNotFound("21", spectacularspiderman, false, false); + createIssueIfNotFound("22", spectacularspiderman, false, false); + createIssueIfNotFound("23", spectacularspiderman, false, false); + createIssueIfNotFound("24", spectacularspiderman, false, false); + createIssueIfNotFound("25", spectacularspiderman, false, false); + createIssueIfNotFound("26", spectacularspiderman, false, false); + createIssueIfNotFound("1", soulfire, false, false); + createIssueIfNotFound("2", soulfire, false, false); + createIssueIfNotFound("3", soulfire, false, false); + createIssueIfNotFound("4", soulfire, false, false); + createIssueIfNotFound("5", soulfire, false, false); + createIssueIfNotFound("1", soulfirelight, false, false); + createIssueIfNotFound("2", soulfirelight, false, false); + createIssueIfNotFound("3", soulfirelight, false, false); + createIssueIfNotFound("4", soulfirelight, false, false); + createIssueIfNotFound("1", spellbinders, false, false); + createIssueIfNotFound("2", spellbinders, false, false); + createIssueIfNotFound("3", spellbinders, false, false); + createIssueIfNotFound("4", spellbinders, false, false); + createIssueIfNotFound("5", spellbinders, false, false); + createIssueIfNotFound("6", spellbinders, false, false); + createIssueIfNotFound("1", spidermanlovesmary, false, false); + createIssueIfNotFound("1", spidermanindia, false, false); + createIssueIfNotFound("2", spidermanindia, false, false); + createIssueIfNotFound("3", spidermanindia, false, false); + createIssueIfNotFound("4", spidermanindia, false, false); + createIssueIfNotFound("1", spidermanteam, false, false); + createIssueIfNotFound("2", spidermanteam, false, false); + createIssueIfNotFound("3", spidermanteam, false, false); + createIssueIfNotFound("4", spidermanteam, false, false); + createIssueIfNotFound("5", spidermanteam, false, false); + createIssueIfNotFound("1", spidermanbreakout, false, false); + createIssueIfNotFound("2", spidermanbreakout, false, false); + createIssueIfNotFound("3", spidermanbreakout, false, false); + createIssueIfNotFound("4", spidermanbreakout, false, false); + createIssueIfNotFound("5", spidermanbreakout, false, false); + createIssueIfNotFound("1", spidermanhousem, false, false); + createIssueIfNotFound("2", spidermanhousem, false, false); + createIssueIfNotFound("3", spidermanhousem, false, false); + createIssueIfNotFound("4", spidermanhousem, false, false); + createIssueIfNotFound("13", sojourn, false, false); + createIssueIfNotFound("14", sojourn, false, false); + createIssueIfNotFound("15", sojourn, false, false); + createIssueIfNotFound("16", sojourn, false, false); + createIssueIfNotFound("17", sojourn, false, false); + createIssueIfNotFound("18", sojourn, false, false); + createIssueIfNotFound("19", sojourn, false, false); + createIssueIfNotFound("20", sojourn, false, false); + createIssueIfNotFound("21", sojourn, false, false); + createIssueIfNotFound("22", sojourn, false, false); + createIssueIfNotFound("23", sojourn, false, false); + createIssueIfNotFound("24", sojourn, false, false); + createIssueIfNotFound("25", sojourn, false, false); + createIssueIfNotFound("26", sojourn, false, false); + createIssueIfNotFound("27", sojourn, false, false); + createIssueIfNotFound("28", sojourn, false, false); + createIssueIfNotFound("29", sojourn, false, false); + createIssueIfNotFound("30", sojourn, false, false); + createIssueIfNotFound("31", sojourn, false, false); + createIssueIfNotFound("32", sojourn, false, false); + createIssueIfNotFound("33", sojourn, false, false); + createIssueIfNotFound("34", sojourn, false, false); + createIssueIfNotFound("1", shrek, false, false); + createIssueIfNotFound("2", shrek, false, false); + createIssueIfNotFound("1", shanna, false, false); + createIssueIfNotFound("2", shanna, false, false); + createIssueIfNotFound("3", shanna, false, false); + createIssueIfNotFound("4", shanna, false, false); + createIssueIfNotFound("5", shanna, false, false); + createIssueIfNotFound("6", shanna, false, false); + createIssueIfNotFound("7", shanna, false, false); + createIssueIfNotFound("100", simpsons, false, false); + createIssueIfNotFound("101", simpsons, false, false); + createIssueIfNotFound("102", simpsons, false, false); + createIssueIfNotFound("103", simpsons, false, false); + createIssueIfNotFound("104", simpsons, false, false); + createIssueIfNotFound("105", simpsons, false, false); + createIssueIfNotFound("106", simpsons, false, false); + createIssueIfNotFound("107", simpsons, false, false); + createIssueIfNotFound("108", simpsons, false, false); + createIssueIfNotFound("109", simpsons, false, false); + createIssueIfNotFound("110", simpsons, false, false); + createIssueIfNotFound("111", simpsons, false, false); + createIssueIfNotFound("112", simpsons, false, false); + createIssueIfNotFound("1", newxmenhellions, false, false); + createIssueIfNotFound("2", newxmenhellions, false, false); + createIssueIfNotFound("3", newxmenhellions, false, false); + createIssueIfNotFound("4", newxmenhellions, false, false); + createIssueIfNotFound("1", newxmenacademy, false, false); + createIssueIfNotFound("2", newxmenacademy, false, false); + createIssueIfNotFound("3", newxmenacademy, false, false); + createIssueIfNotFound("4", newxmenacademy, false, false); + createIssueIfNotFound("5", newxmenacademy, false, false); + createIssueIfNotFound("6", newxmenacademy, false, false); + createIssueIfNotFound("7", newxmenacademy, false, false); + createIssueIfNotFound("8", newxmenacademy, false, false); + createIssueIfNotFound("9", newxmenacademy, false, false); + createIssueIfNotFound("10", newxmenacademy, false, false); + createIssueIfNotFound("11", newxmenacademy, false, false); + createIssueIfNotFound("12", newxmenacademy, false, false); + createIssueIfNotFound("13", newxmenacademy, false, false); + createIssueIfNotFound("14", newxmenacademy, false, false); + createIssueIfNotFound("15", newxmenacademy, false, false); + createIssueIfNotFound("16", newxmenacademy, false, false); + createIssueIfNotFound("151", newxmen, false, false); + createIssueIfNotFound("152", newxmen, false, false); + createIssueIfNotFound("153", newxmen, false, false); + createIssueIfNotFound("154", newxmen, false, false); + createIssueIfNotFound("1", redsonja, false, false); + createIssueIfNotFound("2", redsonja, false, false); + createIssueIfNotFound("3", redsonja, false, false); + createIssueIfNotFound("1", revelations, false, false); + createIssueIfNotFound("2", revelations, false, false); + createIssueIfNotFound("3", revelations, false, false); + createIssueIfNotFound("4", revelations, false, false); + createIssueIfNotFound("1", kisskissbangbang, false, false); + createIssueIfNotFound("2", kisskissbangbang, false, false); + createIssueIfNotFound("3", kisskissbangbang, false, false); + createIssueIfNotFound("4", kisskissbangbang, false, false); + createIssueIfNotFound("5", kisskissbangbang, false, false); + createIssueIfNotFound("1", loki, false, false); + createIssueIfNotFound("2", loki, false, false); + createIssueIfNotFound("3", loki, false, false); + createIssueIfNotFound("4", loki, false, false); + createIssueIfNotFound("1", lullaby, false, false); + createIssueIfNotFound("2", lullaby, false, false); + createIssueIfNotFound("3", lullaby, false, false); + createIssueIfNotFound("1", legendofisis, false, false); + createIssueIfNotFound("2", legendofisis, false, false); + createIssueIfNotFound("1", judge, false, false); + createIssueIfNotFound("2", judge, false, false); + createIssueIfNotFound("3", judge, false, false); + createIssueIfNotFound("1", friendlyneighborspider, false, false); + createIssueIfNotFound("2", friendlyneighborspider, false, false); + createIssueIfNotFound("3", friendlyneighborspider, false, false); + createIssueIfNotFound("1", gift, false, false); + createIssueIfNotFound("2", gift, false, false); + createIssueIfNotFound("3", gift, false, false); + createIssueIfNotFound("4", gift, false, false); + createIssueIfNotFound("5", gift, false, false); + createIssueIfNotFound("6", gift, false, false); + createIssueIfNotFound("7", gift, false, false); + createIssueIfNotFound("8", gift, false, false); + createIssueIfNotFound("9", gift, false, false); + createIssueIfNotFound("10", gift, false, false); + createIssueIfNotFound("11", gift, false, false); + createIssueIfNotFound("12", gift, false, false); + createIssueIfNotFound("13", gift, false, false); + createIssueIfNotFound("1/2", fathom, false, false); + createIssueIfNotFound("1", fathom, false, false); + createIssueIfNotFound("2", fathom, false, false); + createIssueIfNotFound("3", fathom, false, false); + createIssueIfNotFound("4", fathom, false, false); + createIssueIfNotFound("5", fathom, false, false); + createIssueIfNotFound("6", fathom, false, false); + createIssueIfNotFound("7", fathom, false, false); + createIssueIfNotFound("8", fathom, false, false); + createIssueIfNotFound("9", fathom, false, false); + createIssueIfNotFound("10", fathom, false, false); + createIssueIfNotFound("11", fathom, false, false); + createIssueIfNotFound("12", fathom, false, false); + createIssueIfNotFound("13", fathom, false, false); + createIssueIfNotFound("14", fathom, false, false); + createIssueIfNotFound("1", fathombeginnings, false, false); + createIssueIfNotFound("1", fathomcannon, false, false); + createIssueIfNotFound("2", fathomcannon, false, false); + createIssueIfNotFound("3", fathomcannon, false, false); + createIssueIfNotFound("1", fathomcannonprelude, false, false); + createIssueIfNotFound("1", fathomdawnofwar, false, false); + createIssueIfNotFound("2", fathomdawnofwar, false, false); + createIssueIfNotFound("3", fathomdawnofwar, false, false); + createIssueIfNotFound("1", fathomprelude, false, false); + createIssueIfNotFound("1", fathomswimsuit, false, false); + createIssueIfNotFound("1", fathomswimsuit2000, false, false); + createIssueIfNotFound("0", fathomvol2, false, false); + createIssueIfNotFound("1", fathomvol2, false, false); + createIssueIfNotFound("2", fathomvol2, false, false); + createIssueIfNotFound("3", fathomvol2, false, false); + createIssueIfNotFound("4", fathomvol2, false, false); + createIssueIfNotFound("5", fathomvol2, false, false); + createIssueIfNotFound("1", flakriot, false, false); + createIssueIfNotFound("1", freshmen, false, false); + createIssueIfNotFound("1", daringescapes, false, false); + createIssueIfNotFound("2", daringescapes, false, false); + createIssueIfNotFound("3", daringescapes, false, false); + createIssueIfNotFound("4", daringescapes, false, false); + createIssueIfNotFound("1", brath, false, false); + createIssueIfNotFound("2", brath, false, false); + createIssueIfNotFound("3", brath, false, false); + createIssueIfNotFound("4", brath, false, false); + createIssueIfNotFound("5", brath, false, false); + createIssueIfNotFound("6", brath, false, false); + createIssueIfNotFound("7", brath, false, false); + createIssueIfNotFound("8", brath, false, false); + createIssueIfNotFound("9", brath, false, false); + createIssueIfNotFound("10", brath, false, false); + createIssueIfNotFound("11", brath, false, false); + createIssueIfNotFound("12", brath, false, false); + createIssueIfNotFound("13", brath, false, false); + createIssueIfNotFound("14", brath, false, false); + createIssueIfNotFound("1", astonishingxmen, false, false); + createIssueIfNotFound("2", astonishingxmen, false, false); + createIssueIfNotFound("3", astonishingxmen, false, false); + createIssueIfNotFound("4", astonishingxmen, false, false); + createIssueIfNotFound("5", astonishingxmen, false, false); + createIssueIfNotFound("6", astonishingxmen, false, false); + createIssueIfNotFound("7", astonishingxmen, false, false); + createIssueIfNotFound("8", astonishingxmen, false, false); + createIssueIfNotFound("9", astonishingxmen, false, false); + createIssueIfNotFound("10", astonishingxmen, false, false); + createIssueIfNotFound("11", astonishingxmen, false, false); + createIssueIfNotFound("12", astonishingxmen, false, false); + createIssueIfNotFound("1", barbarossa, false, false); + createIssueIfNotFound("1", aspenComic, false, false); + createIssueIfNotFound("2", aspenComic, false, false); + createIssueIfNotFound("3", aspenComic, false, false); + createIssueIfNotFound("1", armydarknessreanim, false, false); + createIssueIfNotFound("2", armydarknessreanim, false, false); + createIssueIfNotFound("3", armydarknessreanim, false, false); + createIssueIfNotFound("1", hellcop, false, false); + createIssueIfNotFound("2", hellcop, false, false); + createIssueIfNotFound("3", hellcop, false, false); + createIssueIfNotFound("4", hellcop, false, false); + createIssueIfNotFound("1", housem, false, false); + createIssueIfNotFound("2", housem, false, false); + createIssueIfNotFound("3", housem, false, false); + createIssueIfNotFound("0", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkiller, false, false); + createIssueIfNotFound("2", hunterkiller, false, false); + createIssueIfNotFound("3", hunterkiller, false, false); + createIssueIfNotFound("4", hunterkiller, false, false); + createIssueIfNotFound("1", hunterkillerdossier, false, false); + createIssueIfNotFound("1", ironghost, false, false); + createIssueIfNotFound("1", magdalenavampirella2, false, false); + createIssueIfNotFound("20", marvelknightspider, false, false); + createIssueIfNotFound("39", meridian, false, false); + createIssueIfNotFound("40", meridian, false, false); + createIssueIfNotFound("41", meridian, false, false); + createIssueIfNotFound("42", meridian, false, false); + createIssueIfNotFound("43", meridian, false, false); + createIssueIfNotFound("44", meridian, false, false); + createIssueIfNotFound("1", necromancer, false, false); + createIssueIfNotFound("2", necromancer, false, false); + createIssueIfNotFound("3", necromancer, false, false); + createIssueIfNotFound("1", negationwar, false, false); + createIssueIfNotFound("2", negationwar, false, false); + createIssueIfNotFound("1", mystique, false, false); + createIssueIfNotFound("2", mystique, false, false); + createIssueIfNotFound("3", mystique, false, false); + createIssueIfNotFound("4", mystique, false, false); + createIssueIfNotFound("5", mystique, false, false); + createIssueIfNotFound("6", mystique, false, false); + createIssueIfNotFound("7", mystique, false, false); + createIssueIfNotFound("8", mystique, false, false); + createIssueIfNotFound("9", mystique, false, false); + createIssueIfNotFound("10", mystique, false, false); + createIssueIfNotFound("11", mystique, false, false); + createIssueIfNotFound("12", mystique, false, false); + createIssueIfNotFound("13", mystique, false, false); + createIssueIfNotFound("14", mystique, false, false); + createIssueIfNotFound("15", mystique, false, false); + createIssueIfNotFound("16", mystique, false, false); + createIssueIfNotFound("17", mystique, false, false); + createIssueIfNotFound("18", mystique, false, false); + createIssueIfNotFound("19", mystique, false, false); + createIssueIfNotFound("20", mystique, false, false); + createIssueIfNotFound("21", mystique, false, false); + createIssueIfNotFound("22", mystique, false, false); + createIssueIfNotFound("23", mystique, false, false); + createIssueIfNotFound("24", mystique, false, false); + createIssueIfNotFound("1", abadazad, false, false); + createIssueIfNotFound("2", abadazad, false, false); + createIssueIfNotFound("3", abadazad, false, false); + createIssueIfNotFound("503", amazingspiderman, false, false); + createIssueIfNotFound("504", amazingspiderman, false, false); + createIssueIfNotFound("505", amazingspiderman, false, false); + createIssueIfNotFound("506", amazingspiderman, false, false); + createIssueIfNotFound("507", amazingspiderman, false, false); + createIssueIfNotFound("508", amazingspiderman, false, false); + createIssueIfNotFound("509", amazingspiderman, false, false); + createIssueIfNotFound("510", amazingspiderman, false, false); + createIssueIfNotFound("511", amazingspiderman, false, false); + createIssueIfNotFound("512", amazingspiderman, false, false); + createIssueIfNotFound("513", amazingspiderman, false, false); + createIssueIfNotFound("514", amazingspiderman, false, false); + createIssueIfNotFound("515", amazingspiderman, false, false); + createIssueIfNotFound("516", amazingspiderman, false, false); + createIssueIfNotFound("517", amazingspiderman, false, false); + createIssueIfNotFound("518", amazingspiderman, false, false); + createIssueIfNotFound("519", amazingspiderman, false, false); + createIssueIfNotFound("520", amazingspiderman, false, false); + createIssueIfNotFound("521", amazingspiderman, false, false); + createIssueIfNotFound("522", amazingspiderman, false, false); + createIssueIfNotFound("523", amazingspiderman, false, false); + createIssueIfNotFound("524", amazingspiderman, false, false); + createIssueIfNotFound("525", amazingspiderman, false, false); + createIssueIfNotFound("526", amazingspiderman, false, false); + createIssueIfNotFound("1", dangergirlbackinblack, false, false); + createIssueIfNotFound("2", dangergirlbackinblack, false, false); + createIssueIfNotFound("1", darknesssuperman, false, false); + createIssueIfNotFound("2", darknesssuperman, false, false); + createIssueIfNotFound("1", darknesstombraider, false, false); + createIssueIfNotFound("1", darknessvampirella, false, false); + createIssueIfNotFound("1", darknessblacksails, false, false); + createIssueIfNotFound("17", darknessvol2, false, false); + createIssueIfNotFound("18", darknessvol2, false, false); + createIssueIfNotFound("19", darknessvol2, false, false); + createIssueIfNotFound("20", darknessvol2, false, false); + createIssueIfNotFound("21", darknessvol2, false, false); + createIssueIfNotFound("22", darknessvol2, false, false); + createIssueIfNotFound("23", darknessvol2, false, false); + createIssueIfNotFound("1", aria, false, false); + createIssueIfNotFound("2", aria, false, false); + createIssueIfNotFound("3", aria, false, false); + createIssueIfNotFound("4", aria, false, false); + createIssueIfNotFound("1", ariasoulmarket, false, false); + createIssueIfNotFound("2", ariasoulmarket, false, false); + createIssueIfNotFound("3", ariasoulmarket, false, false); + createIssueIfNotFound("4", ariasoulmarket, false, false); + createIssueIfNotFound("5", ariasoulmarket, false, false); + createIssueIfNotFound("6", ariasoulmarket, false, false); + createIssueIfNotFound("1", ariasummerspell, false, false); + createIssueIfNotFound("2", ariasummerspell, false, false); + createIssueIfNotFound("1", ariaenchantment, false, false); + createIssueIfNotFound("2", ariaenchantment, false, false); + createIssueIfNotFound("3", ariaenchantment, false, false); + createIssueIfNotFound("4", ariaenchantment, false, false); + createIssueIfNotFound("1", armydarknessashes, false, false); + createIssueIfNotFound("2", armydarknessashes, false, false); + createIssueIfNotFound("3", armydarknessashes, false, false); + createIssueIfNotFound("4", armydarknessashes, false, false); + createIssueIfNotFound("1", armydarknessshop, false, false); + createIssueIfNotFound("2", armydarknessshop, false, false); + createIssueIfNotFound("1", hackslashtoys, false, false); + createIssueIfNotFound("2", hackslashtoys, false, false); + createIssueIfNotFound("1", harryjohnson, false, false); + createIssueIfNotFound("4", battlepope, false, false); + createIssueIfNotFound("5", battlepope, false, false); + createIssueIfNotFound("6", battlepope, false, false); + createIssueIfNotFound("7", battlepope, false, false); + createIssueIfNotFound("8", battlepope, false, false); + createIssueIfNotFound("9", battlepope, false, false); + createIssueIfNotFound("10", battlepope, false, false); + createIssueIfNotFound("11", battlepope, false, false); + createIssueIfNotFound("12", battlepope, false, false); + List volumes = volumeRepository.findAll(); + volumes.forEach(volume -> volumeRepository.delete(volume)); + moduleService.setDataImported(ComicConstants.COMICS); + } + + private Artist createArtistIfNotFound(String artistName) { + log.info("createArtistIfNotFound {}", artistName); + Artist artist = artistRepository.findByName(artistName); + if (artist == null) { + log.info("Artist {} not found, will create it", artistName); + artist = new Artist(); + artist.setName(artistName); + artistRepository.save(artist); + } + return artist; + } + + private Publisher createPublisherIfNotFound(String publisherName) { + log.info("createPublisherIfNotFound {}", publisherName); + Publisher publisher = publisherRepository.findByName(publisherName); + if (publisher == null) { + log.info("Publisher {} not found, will create it", publisherName); + publisher = new Publisher(); + publisher.setName(publisherName); + publisherRepository.save(publisher); + } + return publisher; + } + + private Worktype createWorktypeIfNotFound(String workTypeName) { + log.info("createWorktypeIfNotFound {}", workTypeName); + Worktype worktype = worktypeRepository.findByName(workTypeName); + if (worktype == null) { + log.info("Worktype {} not found, will create it", workTypeName); + worktype = new Worktype(); + worktype.setName(workTypeName); + worktypeRepository.save(worktype); + } + return worktype; + } + + private Comic createComicIfNotFound(Publisher publisher, String title, boolean currentOrder, boolean completed) { + log.info("createComicIfNotFound {} {} {} {}", publisher, title, currentOrder, completed); + Comic comic = comicRepository.findByTitleAndPublisher(title, publisher); + if (comic == null) { + log.info("Comic {} from {} not found, will create it", title, publisher.getName()); + comic = new Comic(); + comic.setTitle(title); + comic.setPublisher(publisher); + comic.setCurrentOrder(currentOrder); + comic.setCompleted(completed); + comicRepository.save(comic); + } + return comic; + } + + private ComicWork createComicWorkIfNotFound(Comic comic, Artist artist, Worktype worktype) { + log.info("createComicWorkIfNotFound {} {} {}", comic, artist, worktype); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + if (comicWork == null) { + log.info("ComicWork {} from {} for {} not found, will create it", worktype, artist, comic); + comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + } + return comicWork; + } + + private void createStoryArcIfNotFound(String name, Comic comic) { + log.info("createStoryArcIfNotFound {} {}", comic, name); + StoryArc storyArc = storyArcRepository.findByNameAndComic(name, comic); + if (storyArc == null) { + log.info("StoryArc {} for {} not found, will create it", name, comic); + storyArc = new StoryArc(); + storyArc.setName(name); + storyArc.setComic(comic); + storyArcRepository.save(storyArc); + } + } + + private void createTradePaperbackIfNotFound(String name, Comic comic, int start, int end) { + log.info("createTradePaperbackIfNotFound {} {} {}-{}", comic, name, start, end); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(name, comic, start, end); + if (tradePaperback == null) { + log.info("TradePaperback {} for {} with issues {}-{} not found, will create it", name, comic, start, end); + tradePaperback = new TradePaperback(); + tradePaperback.setName(name); + tradePaperback.setComic(comic); + tradePaperback.setIssueStart(start); + tradePaperback.setIssueEnd(end); + tradePaperbackRepository.save(tradePaperback); + } + } + + private void createIssueIfNotFound(String issueNumber, Comic comic, boolean isRead, boolean inStock) { + log.info("createIssueIfNotFound {} {} {} {}", comic, issueNumber, isRead, inStock); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, issueNumber); + if (issue == null) { + log.info("Issue {} for {} not found, will create it", issueNumber, comic); + issue = new Issue(); + issue.setIssueNumber(issueNumber); + issue.setComic(comic); + issue.setIsRead(isRead); + issue.setInStock(inStock); + issueRepository.save(issue); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Artist.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Artist.java new file mode 100644 index 0000000..3c2286f --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Artist.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents an artist in the system. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class Artist extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "artist", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Artist{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java new file mode 100644 index 0000000..a5b94f2 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ArtistRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ArtistRepository extends JpaRepository { + @Query("select a from Artist a " + + "where lower(a.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + Artist findByName(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Comic.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Comic.java new file mode 100644 index 0000000..6b04649 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Comic.java @@ -0,0 +1,81 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a comic entity. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Comic extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String title; + + @ManyToOne + @JoinColumn(name = "publisher_id") + @NotNull + @JsonIgnoreProperties({ "comics" }) + private Publisher publisher; + + private Boolean currentOrder = false; + + private Boolean completed = false; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List storyArcs = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List tradePaperbacks = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "comic", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + private List volumes = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Comic{"); + sb.append("title='").append(title).append('\''); + sb.append(", publisher=").append(publisher); + sb.append(", currentOrder=").append(currentOrder); + sb.append(", completed=").append(completed); + sb.append('}'); + return sb.toString(); + } + + public String getPublisherName() { + return this.publisher.getName(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java new file mode 100644 index 0000000..3a3e61f --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicRepository.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface ComicRepository extends JpaRepository { + @Query("select c from Comic c " + + "where lower(c.title) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Comic findByTitleAndPublisher(String title, Publisher publisher); + + List findByTitle(String title); + + List findByTitleIgnoreCase(String title); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java new file mode 100644 index 0000000..840f93c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWork.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "comic_id, artist_id, workType_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"comic_id", "artist_id", "workType_id" }) +) +public class ComicWork extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + @ManyToOne + @JoinColumn(name = "artist_id") + @NotNull + private Artist artist; + + @ManyToOne + @JoinColumn(name = "workType_id") + @NotNull + private Worktype workType; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ComicWork{"); + sb.append("comic=").append(comic); + sb.append(", artist=").append(artist); + sb.append(", workType=").append(workType); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java new file mode 100644 index 0000000..6b3540a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/ComicWorkRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ComicWorkRepository extends JpaRepository { + + @Query("SELECT c from ComicWork c where c.comic = ?1 and c.artist = ?2 and c.workType = ?3") + ComicWork findbyComicAndArtistAndWorktype(Comic comic, Artist artist, Worktype worktype); + + @Query("select c from ComicWork c where c.comic = ?1") + List findByComic(Comic comic); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Issue.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Issue.java new file mode 100644 index 0000000..d63f924 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Issue.java @@ -0,0 +1,46 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Issue extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "issues" }) + private Comic comic; + + @ManyToOne + @JoinColumn(name = "volume_id") + @JsonIgnoreProperties({ "issues" }) + @Nullable + private Volume volume; + + @NotEmpty + private String issueNumber; + + private Boolean isRead; + + private Boolean inStock; + + public String getComicTitle() { + return comic.getTitle(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java new file mode 100644 index 0000000..af10705 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/IssueRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface IssueRepository extends JpaRepository { + @Query("select i from Issue i " + + "where lower(i.issueNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + Issue findByComicAndIssueNumber(Comic comic, String issueNumber); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java new file mode 100644 index 0000000..233d405 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Publisher.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Publisher extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true) + @Nullable + private List comics = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Publisher{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java new file mode 100644 index 0000000..5b5587d --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/PublisherRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PublisherRepository extends JpaRepository { + @Query("select p from Publisher p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Publisher findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java new file mode 100644 index 0000000..34f2188 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArc.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.comics.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class StoryArc extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "storyArcs" }) + private Comic comic; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java new file mode 100644 index 0000000..a74619f --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/StoryArcRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface StoryArcRepository extends JpaRepository { + @Query("select s from StoryArc s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + StoryArc findByNameAndComic(String name, Comic comic); + + List findByComic(Comic comic); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java new file mode 100644 index 0000000..07ee8d3 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperback.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.comics.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class TradePaperback extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + private Comic comic; + + private Integer issueStart; + + private Integer issueEnd; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java new file mode 100644 index 0000000..fba26ad --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/TradePaperbackRepository.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface TradePaperbackRepository extends JpaRepository { + @Query("select t from TradePaperback t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByComic(Comic comic); + + List findByNameAndComic(String name, Comic comic); + + @Query("select t from TradePaperback t where t.name = ?1 and t.comic = ?2 and t.issueStart = ?3 and t.issueEnd = ?4") + TradePaperback findByFields(String name, Comic comic, int start, int end); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Volume.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Volume.java new file mode 100644 index 0000000..c8e1eb0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Volume.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = false) +@Entity +public class Volume extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne + @JoinColumn(name = "comic_id") + @NotNull + @JsonIgnoreProperties({ "volumes" }) + private Comic comic; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "volume", cascade = CascadeType.REMOVE, orphanRemoval = true) + @Nullable + private List issues = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java new file mode 100644 index 0000000..3b70670 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/VolumeRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface VolumeRepository extends JpaRepository { + + List findByName(String name); + + List findByComic(Comic comic); + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java new file mode 100644 index 0000000..9c83133 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/Worktype.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import io.micrometer.common.lang.Nullable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToMany; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a work type in the application. + * This class extends the AbstractEntity class. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +public class Worktype extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "workType", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List comicWorks = new LinkedList<>(); + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("Worktype{"); + sb.append("name='").append(name).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java new file mode 100644 index 0000000..2afebaf --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/data/WorktypeRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.comics.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface WorktypeRepository extends JpaRepository { + @Query("select w from Worktype w " + + "where lower(w.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + + Worktype findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java new file mode 100644 index 0000000..ae4637f --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/services/ComicService.java @@ -0,0 +1,299 @@ +package de.thpeetz.kontor.comics.services; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ArtistRepository; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicRepository; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.ComicWorkRepository; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.IssueRepository; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.PublisherRepository; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.StoryArcRepository; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.TradePaperbackRepository; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.VolumeRepository; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.data.WorktypeRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class ComicService { + + private final PublisherRepository publisherRepository; + private final ComicRepository comicRepository; + private final ArtistRepository artistRepository; + private final IssueRepository issueRepository; + private final StoryArcRepository storyArcRepository; + private final TradePaperbackRepository tradePaperbackRepository; + private final ComicWorkRepository comicWorkRepository; + private final VolumeRepository volumeRepository; + private final WorktypeRepository worktypeRepository; + + public ComicService(PublisherRepository publisherRepository, ComicRepository comicRepository, + ArtistRepository artistRepository, IssueRepository issueRepository, StoryArcRepository storyArcRepository, + TradePaperbackRepository tradePaperbackRepository, ComicWorkRepository comicWorkRepository, + VolumeRepository volumeRepository, WorktypeRepository worktypeRepository) { + + this.publisherRepository = publisherRepository; + this.comicRepository = comicRepository; + this.artistRepository = artistRepository; + this.issueRepository = issueRepository; + this.storyArcRepository = storyArcRepository; + this.tradePaperbackRepository = tradePaperbackRepository; + this.comicWorkRepository = comicWorkRepository; + this.volumeRepository = volumeRepository; + this.worktypeRepository = worktypeRepository; + } + + public List findAllPublishers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return publisherRepository.findAll(); + } else { + return publisherRepository.search(stringFilter); + } + } + + public Publisher findPublisherByName(String publisherName) { + return publisherRepository.findByName(publisherName); + } + + public void deletePublisher(Publisher publisher) { + publisherRepository.delete(publisher); + } + + public void savePublisher(Publisher publisher) { + if (publisher == null) { + log.warn("Publisher is null. Are you sure you have connected your form to the application?"); + return; + } + publisherRepository.save(publisher); + } + + public List findAllComics(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return comicRepository.findAll(); + } else { + return comicRepository.search(stringFilter); + } + } + + public Comic findComicByTitle(String title) { + List comics = comicRepository.findByTitle(title); + if (comics.size() == 1) { + return comics.get(0); + } + return null; + } + + public void deleteComic(Comic comic) { + Publisher publisher = comic.getPublisher(); + publisher.getComics().remove(comic); + publisherRepository.save(publisher); + comicRepository.delete(comic); + } + + public void saveComic(Comic comic) { + if (comic == null) { + log.warn("Comic is null. Are you sure you have connected your form to the application?"); + return; + } + comicRepository.save(comic); + } + + public List findAllArtists(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return artistRepository.findAll(); + } else { + return artistRepository.search(stringFilter); + } + } + + public Artist findArtistByName(String artistName) { + if (artistName == null || artistName.isEmpty()) { + return null; + } else { + return artistRepository.findByName(artistName); + } + } + + public void deleteArtist(Artist artist) { + artistRepository.delete(artist); + } + + public void saveArtist(Artist artist) { + if (artist == null) { + log.warn("Artist is null. Are you sure you have connected your form to the application?"); + return; + } + artistRepository.save(artist); + } + + public List findAllIssues() { + return issueRepository.findAll(); + } + + public List findAllIssuesForComic(Comic comic) { + if (comic == null) { + return issueRepository.findAll(); + } else { + log.info("Find issues for Comic: {}", comic); + return issueRepository.findByComic(comic); + } + } + + public void saveIssue(Issue issue) { + if (issue == null) { + log.warn("Issue is null. Are you sure you have connected your form to the application?"); + return; + } + issueRepository.save(issue); + } + + public void deleteIssue(Issue issue) { + issueRepository.delete(issue); + } + + public List findAllStoryArcs() { + return storyArcRepository.findAll(); + } + + public List findAllStoryArcsForComic(Comic comic) { + if (comic == null) { + return storyArcRepository.findAll(); + } else { + log.info("Find Story Arc for Comic: {}", comic); + return storyArcRepository.findByComic(comic); + } + } + + public void saveStoryArc(StoryArc storyArc) { + storyArcRepository.save(storyArc); + } + + public void deleteStoryArc(StoryArc storyArc) { + Comic comic = storyArc.getComic(); + comic.getStoryArcs().remove(storyArc); + comicRepository.save(comic); + storyArcRepository.delete(storyArc); + } + + public List findAllTradePaperbacks(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return tradePaperbackRepository.findAll(); + } else { + return tradePaperbackRepository.search(stringFilter); + } + } + + public void deleteTradePaperBack(TradePaperback tradepaperback) { + tradePaperbackRepository.delete(tradepaperback); + } + + public void saveTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + log.warn("TradePaperBack is null. Are you sure you have connected your form to the application?"); + return; + } + tradePaperbackRepository.save(tradepaperback); + } + + public List findAllComicWorks() { + return comicWorkRepository.findAll(); + } + + public void saveComicWork(ComicWork comicWork) { + if (comicWork == null) { + log.warn("ComicWork is null. Are you sure you have connected your form to the application?"); + return; + } + comicWorkRepository.save(comicWork); + } + + public void deleteComicWork(ComicWork comicWork) { + Comic comic = comicWork.getComic(); + comic.getComicWorks().remove(comicWork); + comicRepository.save(comic); + Artist artist = comicWork.getArtist(); + artist.getComicWorks().remove(comicWork); + artistRepository.save(artist); + Worktype worktype = comicWork.getWorkType(); + worktype.getComicWorks().remove(comicWork); + worktypeRepository.save(worktype); + comicWorkRepository.delete(comicWork); + } + + public List findAllVolumes() { + return volumeRepository.findAll(); + } + + public List findAllVolumesForComic(Comic comic) { + if (comic == null) { + return volumeRepository.findAll(); + } else { + log.info("Find Volume for Comic: {}", comic); + return volumeRepository.findByComic(comic); + } + } + + public void saveVolume(Volume volume) { + if (volume == null) { + log.warn("Volume is null. Are you sure you have connected your form to the application?"); + return; + } + volumeRepository.save(volume); + } + + public void deleteVolume(Volume volume) { + Comic comic = volume.getComic(); + comic.getVolumes().remove(volume); + comicRepository.save(comic); + volumeRepository.delete(volume); + } + + public List findAllWorktypes(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return worktypeRepository.findAll(); + } else { + return worktypeRepository.search(stringFilter); + } + } + + public Worktype findWorktypeByName(String worktypeName) { + if (worktypeName == null || worktypeName.isEmpty()) { + return null; + } else { + return worktypeRepository.findByName(worktypeName); + } + } + + public void saveWorktype(Worktype worktype) { + if (worktype == null) { + log.warn("Worktype is null. Are you sure you have connected your form to the application?"); + return; + } + worktypeRepository.save(worktype); + } + + public void deleteWorktype(Worktype worktype) { + List comicWorks = worktype.getComicWorks(); + if (comicWorks == null) { + log.warn("reference to ComicWork is null"); + return; + } + log.info("found {} references to ComicWork", comicWorks.size()); + comicWorks.forEach(comicWork -> { + comicWork.setWorkType(null); + }); + log.info("delete Worktype: {}", worktype); + worktypeRepository.delete(worktype); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java new file mode 100644 index 0000000..43a93d4 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.ComicWork; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ArtistForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Artist.class); + + public ArtistForm() { + addClassName("artist-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("workType.name", "comic.title"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setArtist(Artist artist) { + binder.setBean(artist); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class ArtistFormEvent extends ComponentEvent { + private Artist artist; + + protected ArtistFormEvent(ArtistForm source, Artist artist) { + super(source, false); + this.artist = artist; + } + + public Artist getArtist() { + return artist; + } + } + + public static class SaveEvent extends ArtistFormEvent { + SaveEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class DeleteEvent extends ArtistFormEvent { + DeleteEvent(ArtistForm source, Artist artist) { + super(source, artist); + } + } + + public static class CloseEvent extends ArtistFormEvent { + CloseEvent(ArtistForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java new file mode 100644 index 0000000..1a6b2a5 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ArtistView.java @@ -0,0 +1,124 @@ +package de.thpeetz.kontor.comics.views; + +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = "artist", layout = MainLayout.class) +@PageTitle("Artist | Comics | Kontor") +public class ArtistView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Artist.class); + TextField filterText = new TextField(); + @Getter + ArtistForm form; + ComicService service; + + public ArtistView(ComicService service) { + this.service = service; + addClassName("artist-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("artist-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editArtist(event.getValue())); + } + + private void configureForm() { + form = new ArtistForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveArtist); + form.addDeleteListener(this::deleteArtist); + form.addCloseListener(e -> closeEditor()); + } + + private void saveArtist(ArtistForm.SaveEvent event) { + service.saveArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private void deleteArtist(ArtistForm.DeleteEvent event) { + service.deleteArtist(event.getArtist()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addArtistButton = new Button("Add artist"); + addArtistButton.addClickListener(click -> addArtist()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addArtistButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editArtist(Artist artist) { + if (artist == null) { + closeEditor(); + } else { + form.setArtist(artist); + form.setComicWorks(artist.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setArtist(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addArtist() { + grid.asSingleSelect().clear(); + editArtist(new Artist()); + } + + public void updateList() { + grid.setItems(service.findAllArtists(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java new file mode 100644 index 0000000..55fc557 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicForm.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Publisher; + +public class ComicForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(ComicForm.class); + + TextField title = new TextField("Title"); + ComboBox publisher = new ComboBox<>("Publisher"); + Checkbox currentOrder = new Checkbox("Current order"); + Checkbox completed = new Checkbox("Completed"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Comic.class); + + public ComicForm(List publishers) { + addClassName("comic-form"); + binder.bindInstanceFields(this); + + publisher.setItems(publishers); + publisher.setItemLabelGenerator(Publisher::getName); + // comicWorks.addClassName("comic-works-grid"); + // comicWorks.setSizeFull(); + comicWorks.setColumns("workType.name", "artist.name"); + comicWorks.getColumnByKey("workType.name").setHeader("Work type"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + add(title, publisher, currentOrder, completed, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComic(Comic comic) { + binder.setBean(comic); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + comicWorks.setItems(works); + } + + public abstract static class ComicFormEvent extends ComponentEvent { + private Comic comic; + + protected ComicFormEvent(ComicForm source, Comic comic) { + super(source, false); + this.comic = comic; + } + + public Comic getComic() { + return comic; + } + } + + public static class SaveEvent extends ComicFormEvent { + SaveEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class DeleteEvent extends ComicFormEvent { + DeleteEvent(ComicForm source, Comic comic) { + super(source, comic); + } + } + + public static class CloseEvent extends ComicFormEvent { + CloseEvent(ComicForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java new file mode 100644 index 0000000..cffdbdb --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicLayout.java @@ -0,0 +1,43 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class ComicLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public ComicLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(ComicConstants.COMICS); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(ComicConstants.getComicLink(), ComicConstants.getPublisherLink(), ComicConstants.getIssueLink(), + ComicConstants.getTradePaperbackLink(), ComicConstants.getStoryArcLink(), + ComicConstants.getVolumeLink(), ComicConstants.getArtistLink(), ComicConstants.getComicWorkLink(), + ComicConstants.getWorktypeLink()); + return navigation; + } +} + diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java new file mode 100644 index 0000000..b89cfde --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicView.java @@ -0,0 +1,161 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.StatusIcon; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICS_ROUTE, layout = MainLayout.class) +@PageTitle("Comic | Comics | Kontor") +public class ComicView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Comic.class, false); + Grid.Column idColumn = grid.addColumn(Comic::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(Comic::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(Comic::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(Comic::getTitle) + .setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column publisherColumn = grid.addColumn(Comic::getPublisherName) + .setHeader("Verlag").setResizable(true).setSortable(true); + Grid.Column currentOrderColumn = grid.addComponentColumn(comic -> StatusIcon.create(comic.getCurrentOrder())) + .setHeader("Bestellung").setWidth("6rem").setSortable(true); + Grid.Column completedColumn = grid.addComponentColumn(comic -> StatusIcon.create(comic.getCompleted())) + .setHeader("Abgeschlossen").setWidth("6rem").setSortable(true); + TextField filterText = new TextField(); + @Getter + ComicForm form; + ComicService service; + + public ComicView(ComicService service) { + this.service = service; + addClassName("comic-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComic(event.getValue())); + } + + private void configureForm() { + form = new ComicForm(service.findAllPublishers(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComic); + form.addDeleteListener(this::deleteComic); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComic(ComicForm.SaveEvent event) { + service.saveComic(event.getComic()); + updateList(); + closeEditor(); + } + + private void deleteComic(ComicForm.DeleteEvent event) { + service.deleteComic(event.getComic()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addComicButton = new Button("Add comic"); + addComicButton.addClickListener(click -> addComic()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(createdColumn); + columnToggleContextMenu.addColumnToggleItem(modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(titleColumn); + columnToggleContextMenu.addColumnToggleItem(publisherColumn); + columnToggleContextMenu.addColumnToggleItem(currentOrderColumn); + columnToggleContextMenu.addColumnToggleItem(completedColumn); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addComicButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComic(Comic comic) { + if (comic == null) { + closeEditor(); + } else { + form.setComic(comic); + if (comic.getComicWorks() == null) { + log.info("No comic works"); + } else { + log.info("Comic works sze: {}", comic.getComicWorks().size()); + } + form.setComicWorks(comic.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComic(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComic() { + grid.asSingleSelect().clear(); + editComic(new Comic()); + } + + public void updateList() { + grid.setItems(service.findAllComics(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java new file mode 100644 index 0000000..698a37e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class ComicWorkForm extends FormLayout { + ComboBox comic = new ComboBox<>("Comic"); + ComboBox artist = new ComboBox<>("Artist"); + ComboBox workType = new ComboBox<>("Worktype"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(ComicWork.class); + + public ComicWorkForm(List comics, List artists, List workTypes) { + addClassName("comicwork-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + artist.setItems(artists); + artist.setItemLabelGenerator(Artist::getName); + workType.setItems(workTypes); + workType.setItemLabelGenerator(Worktype::getName); + add(comic, artist, workType, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setComicWork(ComicWork comicWork) { + binder.setBean(comicWork); + } + + public abstract static class ComicWorkFormEvent extends ComponentEvent { + private ComicWork comicWork; + + protected ComicWorkFormEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, false); + this.comicWork = comicWork; + } + + public ComicWork getComicWork() { + return comicWork; + } + } + + public static class SaveEvent extends ComicWorkFormEvent { + SaveEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class DeleteEvent extends ComicWorkFormEvent { + DeleteEvent(ComicWorkForm source, ComicWork comicWork) { + super(source, comicWork); + } + } + + public static class CloseEvent extends ComicWorkFormEvent { + CloseEvent(ComicWorkForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java new file mode 100644 index 0000000..449a76c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/ComicWorkView.java @@ -0,0 +1,122 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.COMICWORK_ROUTE, layout = MainLayout.class) +@PageTitle("ComicWork | Comics | Kontor") +public class ComicWorkView extends VerticalLayout { + + Grid grid = new Grid<>(ComicWork.class); + ComicWorkForm form; + ComicService service; + + public ComicWorkView(ComicService service) { + this.service = service; + addClassName("comicWork-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("comic-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "artist.name", "workType.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editComicWork(event.getValue())); + } + + public ComicWorkForm getForm() { + return form; + } + + private void configureForm() { + form = new ComicWorkForm(service.findAllComics(null), service.findAllArtists(null), + service.findAllWorktypes(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveComicWork); + form.addDeleteListener(this::deleteComicWork); + form.addCloseListener(e -> closeEditor()); + } + + private void saveComicWork(ComicWorkForm.SaveEvent event) { + service.saveComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private void deleteComicWork(ComicWorkForm.DeleteEvent event) { + service.deleteComicWork(event.getComicWork()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addComicButton = new Button("Add ComicWork"); + addComicButton.addClickListener(click -> addComicWork()); + + HorizontalLayout toolbar = new HorizontalLayout(addComicButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editComicWork(ComicWork comicWork) { + if (comicWork == null) { + closeEditor(); + } else { + form.setComicWork(comicWork); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setComicWork(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addComicWork() { + grid.asSingleSelect().clear(); + editComicWork(new ComicWork()); + } + + public void updateList() { + grid.setItems(service.findAllComicWorks()); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java new file mode 100644 index 0000000..d537210 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class IssueForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField issueNumber = new TextField("Issue number"); + Checkbox isRead = new Checkbox("Read"); + Checkbox inStock = new Checkbox("In stock"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Issue.class); + + public IssueForm(List comics) { + addClassName("issue-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, issueNumber, isRead, inStock, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setIssue(Issue issue) { + binder.setBean(issue); + } + + public abstract static class IssueFormEvent extends ComponentEvent { + private Issue issue; + + protected IssueFormEvent(IssueForm source, Issue issue) { + super(source, false); + this.issue = issue; + } + + public Issue getIssue() { + return issue; + } + } + + public static class SaveEvent extends IssueFormEvent { + SaveEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class DeleteEvent extends IssueFormEvent { + DeleteEvent(IssueForm source, Issue issue) { + super(source, issue); + } + } + + public static class CloseEvent extends IssueFormEvent { + CloseEvent(IssueForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java new file mode 100644 index 0000000..f77bf99 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/IssueView.java @@ -0,0 +1,169 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.GridSortOrder; +import com.vaadin.flow.data.provider.SortDirection; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.StatusIcon; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.ISSUE_ROUTE, layout = MainLayout.class) +@PageTitle("Issue | Comics | Kontor") +public class IssueView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Issue.class, false); + Grid.Column idColumn = grid.addColumn(Issue::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(Issue::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(Issue::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column versionColumn = grid.addColumn(Issue::getVersion) + .setHeader("Version").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(Issue::getComicTitle) + .setHeader("Comic").setResizable(true).setSortable(true); + Grid.Column issueNumberColumn = grid.addColumn(Issue::getIssueNumber) + .setHeader("Heft Nummer").setResizable(true).setSortable(true); + Grid.Column isReadColumn = grid.addComponentColumn(issueColumn -> StatusIcon.create(issueColumn.getIsRead())) + .setHeader("Gelesen?").setWidth("6rem").setSortable(true); + Grid.Column inStockColumn = grid.addComponentColumn(issueColumn -> StatusIcon.create(issueColumn.getInStock())) + .setHeader("Im Bestand?").setWidth("6rem").setSortable(true); + ComboBox comicFilter = new ComboBox<>("Comic"); + @Getter + IssueForm form; + ComicService service; + + public IssueView(ComicService service) { + this.service = service; + addClassName("issue-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("issue-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + createdColumn.setVisible(false); + modifiedColumn.setVisible(false); + versionColumn.setVisible(false); + grid.setMultiSort(true); + List> sortOrder = new ArrayList<>(); + sortOrder.add(new GridSortOrder(titleColumn, SortDirection.ASCENDING)); + sortOrder.add(new GridSortOrder(issueNumberColumn, SortDirection.ASCENDING)); + grid.sort(sortOrder); + grid.asSingleSelect().addValueChangeListener(event -> editIssue(event.getValue())); + } + + private void configureForm() { + form = new IssueForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveIssue); + form.addDeleteListener(this::deleteIssue); + form.addCloseListener(e -> closeEditor()); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.setClearButtonVisible(true); + comicFilter.addValueChangeListener(e -> updateList()); + + Button addIssueButton = new Button("Add issue"); + addIssueButton.addClickListener(click -> addIssue()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(createdColumn); + columnToggleContextMenu.addColumnToggleItem(modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(versionColumn); + columnToggleContextMenu.addColumnToggleItem(titleColumn); + columnToggleContextMenu.addColumnToggleItem(issueNumberColumn); + columnToggleContextMenu.addColumnToggleItem(isReadColumn); + columnToggleContextMenu.addColumnToggleItem(inStockColumn); + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addIssueButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + private void saveIssue(IssueForm.SaveEvent event) { + service.saveIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + private void deleteIssue(IssueForm.DeleteEvent event) { + service.deleteIssue(event.getIssue()); + updateList(); + closeEditor(); + } + + public void editIssue(Issue issue) { + if (issue == null) { + closeEditor(); + } else { + form.setIssue(issue); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setIssue(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addIssue() { + grid.asSingleSelect().clear(); + editIssue(new Issue()); + } + + private void updateList() { + grid.setItems(service.findAllIssuesForComic(comicFilter.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java new file mode 100644 index 0000000..dbd2f62 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherForm.java @@ -0,0 +1,100 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Publisher; + +public class PublisherForm extends FormLayout { + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Publisher.class); + + public PublisherForm() { + addClassName("publisher-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPublisher(Publisher publisher) { + binder.setBean(publisher); + } + + public abstract static class PublisherFormEvent extends ComponentEvent { + private Publisher publisher; + + protected PublisherFormEvent(PublisherForm source, Publisher publisher) { + super(source, false); + this.publisher = publisher; + } + + public Publisher getPublisher() { + return publisher; + } + } + + public static class SaveEvent extends PublisherFormEvent { + SaveEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class DeleteEvent extends PublisherFormEvent { + DeleteEvent(PublisherForm source, Publisher publisher) { + super(source, publisher); + } + } + + public static class CloseEvent extends PublisherFormEvent { + CloseEvent(PublisherForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java new file mode 100644 index 0000000..b716889 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/PublisherView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.PUBLISHER_ROUTE, layout = MainLayout.class) +@PageTitle("Publisher | Comics | Kontor") +public class PublisherView extends VerticalLayout { + + Grid grid = new Grid<>(Publisher.class); + TextField filterText = new TextField(); + PublisherForm form; + + ComicService service; + + public PublisherView(ComicService service) { + this.service = service; + addClassName("publisher-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("publisher-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPublisher(event.getValue())); + } + + private void configureForm() { + form = new PublisherForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePublisher); + form.addDeleteListener(this::deletePublisher); + form.addCloseListener(e -> closeEditor()); + } + + private void savePublisher(PublisherForm.SaveEvent event) { + service.savePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + private void deletePublisher(PublisherForm.DeleteEvent event) { + service.deletePublisher(event.getPublisher()); + updateList(); + closeEditor(); + } + + public Grid getGrid() { + return grid; + } + + public PublisherForm getForm() { + return form; + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPublisherButton = new Button("Add publisher"); + addPublisherButton.addClickListener(click -> addPublisher()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPublisherButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPublisher(Publisher publisher) { + if (publisher == null) { + closeEditor(); + } else { + form.setPublisher(publisher); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPublisher(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPublisher() { + grid.asSingleSelect().clear(); + editPublisher(new Publisher()); + } + + public void updateList() { + grid.setItems(service.findAllPublishers(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java new file mode 100644 index 0000000..786a792 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.comics.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; + +import java.util.List; + +public class StoryArcForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + TextField name = new TextField("Story Arc Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(StoryArc.class); + + public StoryArcForm(List comics) { + addClassName("storyarc-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setStoryArc(StoryArc storyArc) { + binder.setBean(storyArc); + } + + public abstract static class StoryArcFormEvent extends ComponentEvent { + private StoryArc storyArc; + + protected StoryArcFormEvent(StoryArcForm source, StoryArc storyArc) { + super(source, false); + this.storyArc = storyArc; + } + + public StoryArc getStoryArc() { + return storyArc; + } + } + + public static class SaveEvent extends StoryArcFormEvent { + SaveEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class DeleteEvent extends StoryArcFormEvent { + DeleteEvent(StoryArcForm source, StoryArc storyArc) { + super(source, storyArc); + } + } + + public static class CloseEvent extends StoryArcFormEvent { + CloseEvent(StoryArcForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java new file mode 100644 index 0000000..6044383 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/StoryArcView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.STORYARC_ROTE, layout = MainLayout.class) +@PageTitle("StoryArc | Comics | Kontor") +public class StoryArcView extends VerticalLayout { + + Grid grid = new Grid<>(StoryArc.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + StoryArcForm form; + ComicService service; + + public StoryArcView(ComicService service) { + this.service = service; + addClassName("storyarc-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + StoryArcForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("storyarc-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editStoryArc(event.getValue())); + } + + private void configureForm() { + form = new StoryArcForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveStoryArc); + form.addDeleteListener(this::deleteStoryArc); + form.addCloseListener(e -> closeEditor()); + } + + private void saveStoryArc(StoryArcForm.SaveEvent event) { + service.saveStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private void deleteStoryArc(StoryArcForm.DeleteEvent event) { + service.deleteStoryArc(event.getStoryArc()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addStoryArcButton = new Button("Add StoryArc", click -> addStoryArc()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addStoryArcButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editStoryArc(StoryArc storyArc) { + if (storyArc == null) { + closeEditor(); + } else { + form.setStoryArc(storyArc); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setStoryArc(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addStoryArc() { + grid.asSingleSelect().clear(); + editStoryArc(new StoryArc()); + } + + private void updateList() { + grid.setItems(service.findAllStoryArcsForComic(comicFilter.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java new file mode 100644 index 0000000..8a1b477 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperBackForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.TradePaperback; + +public class TradePaperBackForm extends FormLayout { + TextField name = new TextField("Name"); + ComboBox comic = new ComboBox<>("Comic"); + TextField issueStart = new TextField("Issue Start"); + TextField issueEnd = new TextField("Issue End"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(TradePaperback.class); + + public TradePaperBackForm(List comics) { + addClassName("tradepaperback-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(name, comic, issueStart, issueEnd, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTradePaperBack(TradePaperback tradepaperback) { + binder.setBean(tradepaperback); + } + + public abstract static class TradePaperBackFormEvent extends ComponentEvent { + private TradePaperback tradepaperback; + + protected TradePaperBackFormEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, false); + this.tradepaperback = tradepaperback; + } + + public TradePaperback getTradePaperBack() { + return tradepaperback; + } + } + + public static class SaveEvent extends TradePaperBackFormEvent { + SaveEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class DeleteEvent extends TradePaperBackFormEvent { + DeleteEvent(TradePaperBackForm source, TradePaperback tradepaperback) { + super(source, tradepaperback); + } + } + + public static class CloseEvent extends TradePaperBackFormEvent { + CloseEvent(TradePaperBackForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java new file mode 100644 index 0000000..0215234 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/TradePaperbackView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.TPB_ROUTE, layout = MainLayout.class) +@PageTitle("TradePaperBack | Comics | Kontor") +public class TradePaperbackView extends VerticalLayout { + + Grid grid = new Grid<>(TradePaperback.class); + TextField filterText = new TextField(); + TradePaperBackForm form; + ComicService service; + + public TradePaperbackView(ComicService service) { + this.service = service; + addClassName("tradepaperback-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("tradepaperback-grid"); + grid.setSizeFull(); + grid.setColumns("name", "comic.title", "issueStart", "issueEnd"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTradePaperBack(event.getValue())); + } + + public TradePaperBackForm getForm() { + return form; + } + + private void configureForm() { + form = new TradePaperBackForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTradePaperBack); + form.addDeleteListener(this::deleteTradePaperBack); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTradePaperBack(TradePaperBackForm.SaveEvent event) { + service.saveTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private void deleteTradePaperBack(TradePaperBackForm.DeleteEvent event) { + service.deleteTradePaperBack(event.getTradePaperBack()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTradePaperBackButton = new Button("Add TradePaperBack"); + addTradePaperBackButton.addClickListener(click -> addTradePaperBack()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTradePaperBackButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTradePaperBack(TradePaperback tradepaperback) { + if (tradepaperback == null) { + closeEditor(); + } else { + form.setTradePaperBack(tradepaperback); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTradePaperBack(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTradePaperBack() { + grid.asSingleSelect().clear(); + editTradePaperBack(new TradePaperback()); + } + + public void updateList() { + grid.setItems(service.findAllTradePaperbacks(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java new file mode 100644 index 0000000..9b333a9 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeForm.java @@ -0,0 +1,109 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.Volume; + +public class VolumeForm extends FormLayout { + + ComboBox comic = new ComboBox<>("Comic"); + public TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Volume.class); + + public VolumeForm(List comics) { + addClassName("volume-form"); + binder.bindInstanceFields(this); + + comic.setItems(comics); + comic.setItemLabelGenerator(Comic::getTitle); + add(comic, name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVolume(Volume volume) { + binder.setBean(volume); + } + + public abstract static class VolumeFormEvent extends ComponentEvent { + private Volume volume; + + protected VolumeFormEvent(VolumeForm source, Volume volume) { + super(source, false); + this.volume = volume; + } + + public Volume getVolume() { + return volume; + } + } + + public static class SaveEvent extends VolumeFormEvent { + SaveEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class DeleteEvent extends VolumeFormEvent { + DeleteEvent(VolumeForm source, Volume volume) { + super(source, volume); + } + } + + public static class CloseEvent extends VolumeFormEvent { + CloseEvent(VolumeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java new file mode 100644 index 0000000..47a2201 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/VolumeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.VOLUME_ROUTE, layout = MainLayout.class) +@PageTitle("Volume | Comics | Kontor") +public class VolumeView extends VerticalLayout { + + Grid grid = new Grid<>(Volume.class); + ComboBox comicFilter = new ComboBox<>("Comic"); + VolumeForm form; + ComicService service; + + public VolumeView(ComicService service) { + this.service = service; + addClassName("volume-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + VolumeForm getForm() { + return form; + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("volume-grid"); + grid.setSizeFull(); + grid.setColumns("comic.title", "name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVolume(event.getValue())); + } + + private void configureForm() { + form = new VolumeForm(service.findAllComics(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVolume); + form.addDeleteListener(this::deleteVolume); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVolume(VolumeForm.SaveEvent event) { + service.saveVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private void deleteVolume(VolumeForm.DeleteEvent event) { + service.deleteVolume(event.getVolume()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + comicFilter.setItems(service.findAllComics(null)); + comicFilter.setItemLabelGenerator(Comic::getTitle); + comicFilter.addValueChangeListener(e -> updateList()); + comicFilter.setClearButtonVisible(true); + + Button addVolumeButton = new Button("Add Volume", click -> addVolume()); + + HorizontalLayout toolbar = new HorizontalLayout(comicFilter, addVolumeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVolume(Volume volume) { + if (volume == null) { + closeEditor(); + } else { + form.setVolume(volume); + form.setVisible(true); + addClassName("editing"); + } + } + + public void closeEditor() { + form.setVolume(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVolume() { + grid.asSingleSelect().clear(); + editVolume(new Volume()); + } + + private void updateList() { + grid.setItems(service.findAllVolumesForComic(comicFilter.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java new file mode 100644 index 0000000..0965229 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeForm.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.comics.views; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Worktype; + +public class WorktypeForm extends FormLayout { + + private static final Logger log = LoggerFactory.getLogger(WorktypeForm.class); + + TextField name = new TextField("Name"); + Grid comicWorks = new Grid<>(ComicWork.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Worktype.class); + + public WorktypeForm() { + addClassName("worktype-form"); + binder.bindInstanceFields(this); + + comicWorks.setColumns("comic.title", "artist.name"); + comicWorks.getColumnByKey("comic.title").setHeader("Comic"); + comicWorks.getColumnByKey("artist.name").setHeader("Artist"); + comicWorks.getColumns().forEach(col -> col.setAutoWidth(true)); + + add(name, comicWorks, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setWorktype(Worktype worktype) { + binder.setBean(worktype); + } + + public void setComicWorks(List works) { + log.info("Setting comic works: {}", works); + this.comicWorks.setItems(works); + } + + public abstract static class WorktypeFormEvent extends ComponentEvent { + private Worktype worktype; + + protected WorktypeFormEvent(WorktypeForm source, Worktype worktype) { + super(source, false); + this.worktype = worktype; + } + + public Worktype getWorktype() { + return worktype; + } + } + + public static class SaveEvent extends WorktypeFormEvent { + SaveEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class DeleteEvent extends WorktypeFormEvent { + DeleteEvent(WorktypeForm source, Worktype worktype) { + super(source, worktype); + } + } + + public static class CloseEvent extends WorktypeFormEvent { + CloseEvent(WorktypeForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java new file mode 100644 index 0000000..98f3adb --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/comics/views/WorktypeView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.comics.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; +import de.thpeetz.kontor.common.views.MainLayout; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = ComicConstants.WORKTYPE_ROUTE, layout = MainLayout.class) +@PageTitle("Worktype | Comics | Kontor") +public class WorktypeView extends VerticalLayout { + + Grid grid = new Grid<>(Worktype.class); + TextField filterText = new TextField(); + WorktypeForm form; + ComicService service; + + public WorktypeView(ComicService service) { + this.service = service; + addClassName("worktype-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("worktype-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editWorktype(event.getValue())); + } + + public WorktypeForm getForm() { + return form; + } + + private void configureForm() { + form = new WorktypeForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveWorktype); + form.addDeleteListener(this::deleteWorktype); + form.addCloseListener(e -> closeEditor()); + } + + private void saveWorktype(WorktypeForm.SaveEvent event) { + service.saveWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private void deleteWorktype(WorktypeForm.DeleteEvent event) { + service.deleteWorktype(event.getWorktype()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addWorktypeButton = new Button("Add worktype"); + addWorktypeButton.addClickListener(click -> addWorktype()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addWorktypeButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editWorktype(Worktype worktype) { + if (worktype == null) { + closeEditor(); + } else { + form.setWorktype(worktype); + form.setComicWorks(worktype.getComicWorks()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setWorktype(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addWorktype() { + grid.asSingleSelect().clear(); + editWorktype(new Worktype()); + } + + public void updateList() { + grid.setItems(service.findAllWorktypes(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java new file mode 100644 index 0000000..182dd45 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractEntity.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; + + public void updateVersion(String value) { + if (value == null) { + log.info("value not given"); + return; + } + if (version != Integer.valueOf(value).intValue()) { + log.info("update version"); + version = Integer.valueOf(value).intValue(); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java new file mode 100644 index 0000000..7dfdf50 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/data/AbstractLinkEntity.java @@ -0,0 +1,55 @@ +package de.thpeetz.kontor.common.data; + +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.util.Date; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractLinkEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @Version + private int version; + + @CreatedDate + private Date createdDate; + + @LastModifiedDate + private Date lastModifiedDate; + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java new file mode 100644 index 0000000..39459ef --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/AvatarMenuBar.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.contextmenu.SubMenu; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.menubar.MenuBarVariant; +import com.vaadin.flow.router.Route; + +import com.vaadin.flow.router.Router; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.views.UserProfileView; +import de.thpeetz.kontor.security.SecurityService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +@Slf4j +@Route("avatar-menu-bar") +public class AvatarMenuBar extends Div { + + private final SecurityService securityService; + + final AdminService adminService; + + public AvatarMenuBar(SecurityService securityService, AdminService adminService) { + this.adminService = adminService; + this.securityService = securityService; + + Avatar avatar = new Avatar(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("AdminService: {}", adminService); + if (user != null) { + String userName = user.getUsername(); + String fullName = adminService.getProfileFullName(userName); + avatar.setName(fullName); + } + }); + // avatar.setImage(pictureUrl); + MenuBar menuBar = new MenuBar(); + menuBar.addThemeVariants(MenuBarVariant.LUMO_TERTIARY_INLINE); + MenuItem menuItem = menuBar.addItem(avatar); + SubMenu subMenu = menuItem.getSubMenu(); + MenuItem logoutMenuItem = subMenu.addItem("Log out"); + logoutMenuItem.addClickListener(e -> securityService.logout()); + MenuItem profileItem = subMenu.addItem("Profile"); + profileItem.addClickListener(e -> profileItem.getUI().ifPresent(ui -> ui.navigate(UserProfileView.class))); + subMenu.addItem("Settings"); + subMenu.addItem("Help"); + add(menuBar); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/ColumnToggleContextMenu.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/ColumnToggleContextMenu.java new file mode 100644 index 0000000..f2bd74e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/ColumnToggleContextMenu.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; + +public class ColumnToggleContextMenu extends ContextMenu { + public ColumnToggleContextMenu(Component target) { + super(target); + setOpenOnClick(true); + } + + public void addColumnToggleItem(Grid.Column column) { + String label = column.getHeaderText(); + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } + + public void addColumnToggleItem(String label, Grid.Column column) { + MenuItem menuItem = this.addItem(label, e -> { + column.setVisible(e.getSource().isChecked()); + }); + menuItem.setCheckable(true); + menuItem.setChecked(column.isVisible()); + menuItem.setKeepOpen(true); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java new file mode 100644 index 0000000..64c60d2 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/KontorLayoutUtil.java @@ -0,0 +1,99 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.AppLayout.Section; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KontorLayoutUtil { + + private final AppLayout appLayout; + private HorizontalLayout secondaryNavigation; + + private AdminService adminService; + + private SecurityService securityService; + + public KontorLayoutUtil(AppLayout layout, AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + this.appLayout = layout; + } + + public void setSecondaryNavigation(HorizontalLayout secondaryNavigation) { + this.secondaryNavigation = secondaryNavigation; + } + + public void createHeader(String titleName) { + appLayout.addToDrawer(createTitle(), getScroller()); + appLayout.addToNavbar(getHeader(titleName)); + appLayout.setPrimarySection(Section.DRAWER); + } + + private H1 createTitle() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + return appTitle; + } + + private Scroller getScroller() { + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + return scroller; + } + + private VerticalLayout getHeader(String header) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(header); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + HorizontalLayout subViews = this.secondaryNavigation; + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper, subViews); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + return viewHeader; + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java new file mode 100644 index 0000000..95d60cc --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainLayout.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.applayout.DrawerToggle; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.H2; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.Scroller; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.AdminConstants; +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.bookshelf.BookshelfConstants; +import de.thpeetz.kontor.comics.ComicConstants; +import de.thpeetz.kontor.data.views.DataManagementView; +import de.thpeetz.kontor.mailclient.views.EmailView; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; + +@Slf4j +public class MainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public MainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + createHeader("Kontor"); + createDrawer(); + } + + public void createDrawer() { + H1 appTitle = new H1(new RouterLink("Kontor", MainView.class)); + appTitle.getStyle().set("font-size", "var(--lumo-font-size-l)") + .set("line-height", "var(--lumo-size-l)") + .set("margin", "0 var(--lumo-space-m)"); + Scroller scroller = new Scroller(getPrimaryNavigation()); + scroller.setClassName(LumoUtility.Padding.SMALL); + addToDrawer(appTitle, scroller); + } + + public void createHeader(String titleName) { + DrawerToggle toggle = new DrawerToggle(); + H2 viewTitle = new H2(titleName); + viewTitle.addClassNames(LumoUtility.FontSize.LARGE, LumoUtility.Margin.NONE); + + AvatarMenuBar avatar = new AvatarMenuBar(securityService, adminService); + HorizontalLayout wrapper = new HorizontalLayout(toggle, viewTitle, avatar); + wrapper.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER); + wrapper.expand(viewTitle); + wrapper.setWidthFull(); + wrapper.setSpacing(false); + + VerticalLayout viewHeader = new VerticalLayout(wrapper); + viewHeader.setPadding(false); + viewHeader.setSpacing(false); + + addToNavbar(viewHeader); + setPrimarySection(Section.DRAWER); + } + + private SideNav getPrimaryNavigation() { + SideNav sideNav = new SideNav(); + ArrayList roles = new ArrayList<>(); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("Get roles for user {}", user.getUsername()); + user.getAuthorities().forEach(grantedAuthority -> { + roles.add(grantedAuthority.getAuthority()); + }); + log.info("user {} has roles: {}", user, roles); + }); + sideNav.addItem(ComicConstants.getComicsNavigation()); + sideNav.addItem(TyscConstants.getTyscNavigation()); + sideNav.addItem(BookshelfConstants.getBookshelfNavigation()); + sideNav.addItem(MediaConstants.getMediaNavigation(roles)); + sideNav.addItem(new SideNavItem("Emails", EmailView.class)); + sideNav.addItem(new SideNavItem("Data Management", DataManagementView.class, VaadinIcon.DATABASE.create())); + securityService.getAuthenticatedUser().ifPresent(user -> { + log.info("User {} found", user.getUsername()); + boolean isAdmin = user.getAuthorities().stream() + .anyMatch(grantedAuthority -> "ROLE_ADMIN".equals(grantedAuthority.getAuthority())); + log.info("User {} hat Admin-Rechte: {}", user, isAdmin); + if (isAdmin) { + sideNav.addItem(AdminConstants.getAdminNavigation()); + } + }); + return sideNav; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainView.java new file mode 100644 index 0000000..08edac9 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/MainView.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.common.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.annotation.SpringComponent; + +@SpringComponent +@Scope("prototype") +@AnonymousAllowed +@Route(value = "/", layout = MainLayout.class) +@PageTitle("Kontor") +public class MainView extends VerticalLayout { + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java new file mode 100644 index 0000000..474d4a7 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/SeparateMainLayout.java @@ -0,0 +1,31 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.security.SecurityService; + +public class SeparateMainLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public SeparateMainLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader("Kontor"); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + return navigation; + } + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/StatusIcon.java b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/StatusIcon.java new file mode 100644 index 0000000..4e058c6 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/common/views/StatusIcon.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.common.views; + +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; + +public class StatusIcon { + public static Icon create(boolean status) { + Icon icon; + if (status) { + icon = VaadinIcon.CHECK.create(); + icon.getElement().getThemeList().add("badge success"); + } else { + icon = VaadinIcon.CLOSE_SMALL.create(); + icon.getElement().getThemeList().add("badge error"); + } + icon.getStyle().set("padding", "var(--lumo-space-xs"); + return icon; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/data/services/DataManagementService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/data/services/DataManagementService.java new file mode 100644 index 0000000..4b950ac --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/data/services/DataManagementService.java @@ -0,0 +1,46 @@ +package de.thpeetz.kontor.data.services; + +import de.thpeetz.kontor.admin.data.MetaDataTable; +import de.thpeetz.kontor.admin.repository.MetaDataTableRepository; +import de.thpeetz.kontor.admin.services.MetaDataService; +import de.thpeetz.kontor.tysc.services.SportService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +@Slf4j +@Service +public class DataManagementService { + + @Autowired + MetaDataService metaDataService; + + @Autowired + SportService sportService; + + public DataManagementService() { + + } + + public String getEntry(String nodeName, Map fields) { + AtomicReference status = new AtomicReference<>("unknown"); + switch (nodeName) { + case "meta_data_table": + status.set(metaDataService.importTableData(fields)); + break; + case "meta_data_column": + status.set(metaDataService.importColumnData(nodeName, fields)); + break; + case "sport": + status.set(sportService.importData(fields)); + default: + log.debug("import for {} not implemented", nodeName); + break; + } + return status.get(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/DataManagementView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/DataManagementView.java new file mode 100644 index 0000000..43e0675 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/DataManagementView.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.data.views; + +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.data.services.DataManagementService; +import jakarta.annotation.security.PermitAll; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +import java.io.File; + +@Slf4j +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = "common/import", layout = MainLayout.class) +@PageTitle("Import Data | Kontor") +public class DataManagementView extends VerticalLayout { + + DataManagementService service; + + public DataManagementView(DataManagementService service) { + this.service = service; + File uploadFolder = getUploadFolder(); + UploadArea uploadArea = new UploadArea(uploadFolder); + Checkbox pruneTables = new Checkbox("Prune Table"); + ImportArea importArea = new ImportArea(uploadFolder, pruneTables, service); + + uploadArea.getUploadField().addSucceededListener(e -> { + uploadArea.hideErrorField(); + importArea.processFiles(); + uploadArea.getUploadField().clearFileList(); + }); + + add(uploadArea, pruneTables, importArea); + } + + private File getUploadFolder() { + File folder = new File("uploaded-files"); + if (!folder.exists()) { + folder.mkdirs(); + } + return folder; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/ImportArea.java b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/ImportArea.java new file mode 100644 index 0000000..831f6a3 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/ImportArea.java @@ -0,0 +1,111 @@ +package de.thpeetz.kontor.data.views; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.html.H4; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import de.thpeetz.kontor.data.services.DataManagementService; +import lombok.extern.slf4j.Slf4j; +import org.json.simple.parser.ParseException; + +import java.io.*; +import java.util.*; + +@Slf4j +public class ImportArea extends VerticalLayout { + + protected File uploadFolder; + DataManagementService service; + boolean pruneTables = false; + + public ImportArea(File uploadFolder, Checkbox pruneTablesCheckbox, DataManagementService service) { + this.uploadFolder = uploadFolder; + this.service = service; + this.pruneTables = pruneTablesCheckbox.isEnabled(); + setMargin(true); + } + + public void processFiles() { + removeAll(); + if (pruneTables) { + add(new Paragraph("prune all tables")); + } + add(new H4("Process file")); + + for (File file: uploadFolder.listFiles()) { + add(new Paragraph("reading " + file.getName())); + try { + processFile(file); + } catch (ParseException e) { + throw new RuntimeException(e); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private void processFile(File file) throws FileNotFoundException, IOException, ParseException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode node = objectMapper.readTree(file); + log.info("parsing: {}, {}", node.getNodeType(), node.size()); + Iterator fieldNamesIterator = node.fieldNames(); + while (fieldNamesIterator.hasNext()) { + String fieldName = fieldNamesIterator.next(); + parseRoot(node, fieldName); + } + } + + private void parseRoot(JsonNode parentNode, String field) { + JsonNode node = parentNode.get(field); + String nodeType = String.valueOf(node.getNodeType()); + if (nodeType.equals("ARRAY")) { + log.info(" {}: {}", field, node.size()); + Iterator elements = node.elements(); + while (elements.hasNext()) { + JsonNode subNode = elements.next(); + String status = parseNode(subNode, field); + add(new Paragraph(status)); + } + } else { + log.info("unknown type: {}", nodeType); + } + } + + private String parseNode(JsonNode node, String nodeName) { + Map fields = parseFields(node); + return service.getEntry(nodeName, fields); + } + + private Map parseFields(JsonNode node) { + Map fields = new HashMap<>(); + node.fieldNames().forEachRemaining(field -> { + + JsonNode value = node.get(field); + log.debug("type: {}", value.getNodeType()); + JsonNodeType nodeType = value.getNodeType(); + switch (nodeType) { + case NUMBER: + fields.put(field, String.valueOf(value.intValue())); + break; + case STRING: + String stringValue = value.textValue(); + if (stringValue.contains("\"")) { + log.debug("remove double quotes"); + String stripped = stringValue.substring(1, stringValue.length()-1); + fields.put(field, stripped); + } + fields.put(field, value.textValue()); + break; + default: + log.debug("no implementation for type {}", nodeType.name()); + } + }); + log.debug("fields: {}", fields); + return fields; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/UploadArea.java b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/UploadArea.java new file mode 100644 index 0000000..f82efda --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/data/views/UploadArea.java @@ -0,0 +1,58 @@ +package de.thpeetz.kontor.data.views; + +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.upload.MultiFileReceiver; +import com.vaadin.flow.component.upload.Receiver; +import com.vaadin.flow.component.upload.Upload; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +@Slf4j +public class UploadArea extends VerticalLayout { + + @Getter + private final Upload uploadField; + private final Span errorField; + + public UploadArea(File uploadFolder) { + uploadField = new Upload(createFileReceiver(uploadFolder)); + uploadField.setMaxFiles(1); + uploadField.setMaxFileSize(2*1024*1024); + + errorField = new Span(); + errorField.setVisible(false); + errorField.getStyle().set("color", "red"); + + uploadField.addFailedListener(e -> showErrorMessage(e.getReason().getMessage())); + uploadField.addFileRejectedListener(e -> showErrorMessage(e.getErrorMessage())); + + add(uploadField, errorField); + } + + public void hideErrorField() { + errorField.setVisible(false); + } + + private Receiver createFileReceiver(File uploadFolder) { + return (MultiFileReceiver) (filename, mimetype) -> { + File file = new File(uploadFolder, filename); + try { + return new FileOutputStream(file); + } + catch (FileNotFoundException fnfe) { + log.info(fnfe.getStackTrace().toString()); + return null; + } + }; + } + + private void showErrorMessage(String message) { + errorField.setVisible(true); + errorField.setText(message); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java new file mode 100644 index 0000000..f3b80a8 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/Mail.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper=false) +@Entity +public class Mail extends AbstractEntity { + + private String folder; + private String subject; + private String body; + private Date sentDate; + private Date receivedDate; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java new file mode 100644 index 0000000..840d8da --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/data/MailAccount.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.mailclient.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +public class MailAccount extends AbstractEntity { + + @NotEmpty + private String host; + + private Integer port; + + @NotEmpty + private String protocol; + + private String userName; + + private String password; + + private Boolean startTls; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java new file mode 100644 index 0000000..4235dd1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/mailclient/views/EmailView.java @@ -0,0 +1,222 @@ +package de.thpeetz.kontor.mailclient.views; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Properties; + +import javax.mail.Address; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.NoSuchProviderException; +import javax.mail.Session; +import javax.mail.Store; +import javax.mail.internet.InternetAddress; + +import com.sun.mail.imap.IMAPFolder; +import com.vaadin.flow.component.Composite; +import com.vaadin.flow.component.accordion.Accordion; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.menubar.MenuBar; +import com.vaadin.flow.component.messages.MessageList; +import com.vaadin.flow.component.messages.MessageListItem; +import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment; +import com.vaadin.flow.component.orderedlayout.FlexComponent.JustifyContentMode; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.sidenav.SideNav; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouteAlias; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.theme.lumo.LumoUtility.Gap; + +import de.thpeetz.kontor.mailclient.data.MailAccount; +import de.thpeetz.kontor.admin.services.MailService; +import de.thpeetz.kontor.common.views.MainLayout; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@PageTitle("Email") +@AnonymousAllowed +@Route(value = "mail", layout = MainLayout.class) +@RouteAlias(value = "kontor/mail", layout = MainLayout.class) +public class EmailView extends Composite { + + MenuBar menuBar = new MenuBar(); + Accordion accordion = new Accordion(); + SideNav folderList = new SideNav(); + MailService mailService; + + public EmailView(MailService mailService) { + this.mailService = mailService; + configureView(); + updateFolderList(); + } + + private void updateFolderList() { + List accounts = mailService.findAllMailAccounts(); + MailAccount account = accounts.get(0); + + Properties properties = new Properties(); + properties.put(String.format("mail.%s.host", account.getProtocol()), account.getHost()); + properties.put(String.format("mail.%s.port", account.getProtocol()), account.getPort().toString()); + properties.put("mail.imap.starttls.enable", "true"); + + Session session = Session.getDefaultInstance(properties); + Store store; + try { + store = session.getStore(account.getProtocol()); + store.connect(account.getUserName(), account.getPassword()); + Folder rootFolder = store.getDefaultFolder(); + Folder[] folders = rootFolder.list(); + for (Folder value : folders) { + SideNavItem folder = addSubFolder(value); + folderList.addItem(folder); + } + log.info("{} folders found", folders.length); + Folder folder = store.getFolder("INBOX"); + IMAPFolder imapFolder = (IMAPFolder) folder; + imapFolder.open(Folder.READ_ONLY); + Message[] messages = imapFolder.getMessages(); + log.info("Folder {} opened", folder.getFullName()); + int messageCount = folder.getMessageCount(); + log.info("{} messages found", messages.length); + for (Message message : messages) { + log.info("Message {}: {}, {}, {}", message.getMessageNumber(), message.getSubject(), message.getReceivedDate(), message.getSentDate()); + //log.info("Message: {}", message.getAllRecipients()); + Address[] addresses; + printAddress(message.getRecipients(Message.RecipientType.TO)); + printAddress(message.getRecipients(Message.RecipientType.CC)); + printAddress(message.getRecipients(Message.RecipientType.BCC)); + } + store.close(); + log.info("Connection closed"); + } catch (NoSuchProviderException e) { + throw new RuntimeException(e); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + private void printAddress(Address[] addresses) { + if (addresses != null) { + for (Address address: addresses) { + log.info("Address {}: {}", address.getType(), address.toString()); + InternetAddress internetAddress = (InternetAddress)address; + log.info("InternetAddress {}: {}", internetAddress.getPersonal(), internetAddress.getAddress()); + } + } + } + + private SideNavItem addSubFolder(Folder mailFolder) throws MessagingException { + log.debug("addSubFolder: {}", mailFolder.getName()); + Span counter = new Span(String.valueOf(mailFolder.getMessageCount())); + counter.getElement().getThemeList().add("badge contrast pill"); + counter.getElement().setAttribute("aria-label", String.format("%d unread messages", mailFolder.getMessageCount())); + SideNavItem folder = new SideNavItem(mailFolder.getName()); + folder.setSuffixComponent(counter); + Folder[] folders = mailFolder.list(); + log.info("{} subfolders found", folders.length); + for (Folder value : folders) { + SideNavItem subFolder = addSubFolder(value); + folder.addItem(subFolder); + } + return folder; + } + + private void configureView() { + HorizontalLayout layoutMenubar = new HorizontalLayout(); + HorizontalLayout layoutRow2 = new HorizontalLayout(); + VerticalLayout layoutColumn2 = new VerticalLayout(); + VerticalLayout layoutColumn3 = new VerticalLayout(); + MessageList messageList = new MessageList(); + VerticalLayout layoutColumn4 = new VerticalLayout(); + Paragraph textMedium = new Paragraph(); + getContent().setWidth("100%"); + getContent().getStyle().set("flex-grow", "1"); + layoutMenubar.addClassName(Gap.MEDIUM); + layoutMenubar.setWidth("100%"); + layoutMenubar.setHeight("min-content"); + menuBar.setWidth("min-content"); + setMenuBarSampleData(menuBar); + layoutRow2.addClassName(Gap.MEDIUM); + layoutRow2.setWidth("100%"); + layoutRow2.getStyle().set("flex-grow", "1"); + layoutColumn2.getStyle().set("flex-grow", "1"); + accordion.setWidth("100%"); + setAccordionSampleData(accordion); + layoutColumn3.setHeightFull(); + layoutRow2.setFlexGrow(1.0, layoutColumn3); + layoutColumn3.setWidth("100%"); + layoutColumn3.getStyle().set("flex-grow", "1"); + layoutColumn3.setJustifyContentMode(JustifyContentMode.START); + layoutColumn3.setAlignItems(Alignment.START); + messageList.setWidth("100%"); + setMessageListSampleData(messageList); + layoutColumn4.setWidthFull(); + layoutColumn3.setFlexGrow(1.0, layoutColumn4); + layoutColumn4.setWidth("100%"); + layoutColumn4.getStyle().set("flex-grow", "1"); + textMedium.setText( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); + textMedium.setWidth("100%"); + textMedium.getStyle().set("font-size", "var(--lumo-font-size-m)"); + getContent().add(layoutMenubar); + layoutMenubar.add(menuBar); + getContent().add(layoutRow2); + layoutRow2.add(layoutColumn2); + layoutColumn2.add(folderList); + layoutRow2.add(layoutColumn3); + layoutColumn3.add(messageList); + layoutColumn3.add(layoutColumn4); + layoutColumn4.add(textMedium); + } + + private void setMenuBarSampleData(MenuBar menuBar) { + menuBar.addItem("View"); + menuBar.addItem("Edit"); + menuBar.addItem("Share"); + menuBar.addItem("Move"); + } + + private void setAccordionSampleData(Accordion accordion) { + Span name = new Span("Sophia Williams"); + Span email = new Span("sophia.williams@company.com"); + Span phone = new Span("(501) 555-9128"); + VerticalLayout personalInformationLayout = new VerticalLayout(name, email, phone); + personalInformationLayout.setSpacing(false); + personalInformationLayout.setPadding(false); + accordion.add("Personal information", personalInformationLayout); + Span street = new Span("4027 Amber Lake Canyon"); + Span zipCode = new Span("72333-5884 Cozy Nook"); + Span city = new Span("Arkansas"); + VerticalLayout billingAddressLayout = new VerticalLayout(); + billingAddressLayout.setSpacing(false); + billingAddressLayout.setPadding(false); + billingAddressLayout.add(street, zipCode, city); + accordion.add("Billing address", billingAddressLayout); + Span cardBrand = new Span("Mastercard"); + Span cardNumber = new Span("1234 5678 9012 3456"); + Span expiryDate = new Span("Expires 06/21"); + VerticalLayout paymentLayout = new VerticalLayout(); + paymentLayout.setSpacing(false); + paymentLayout.setPadding(false); + paymentLayout.add(cardBrand, cardNumber, expiryDate); + accordion.add("Payment", paymentLayout); + } + + private void setMessageListSampleData(MessageList messageList) { + MessageListItem message1 = new MessageListItem("Nature does not hurry, yet everything gets accomplished.", + LocalDateTime.now().minusDays(1).toInstant(ZoneOffset.UTC), "Matt Mambo"); + message1.setUserColorIndex(1); + MessageListItem message2 = new MessageListItem( + "Using your talent, hobby or profession in a way that makes you contribute with something good to this world is truly the way to go.", + LocalDateTime.now().minusMinutes(55).toInstant(ZoneOffset.UTC), "Linsey Listy"); + message2.setUserColorIndex(2); + messageList.setItems(message1, message2); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/MediaConstants.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/MediaConstants.java new file mode 100644 index 0000000..76fc9bc --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/MediaConstants.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.media; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import de.thpeetz.kontor.media.views.MediaActorView; +import de.thpeetz.kontor.media.views.MediaArticleView; +import de.thpeetz.kontor.media.views.MediaFileView; +import de.thpeetz.kontor.media.views.MediaVideoView; + +import java.util.ArrayList; + +public class MediaConstants { + + public static final String MEDIA = "Media"; + public static final String MEDIAFILE_ROUTE = "media/mediafile"; + public static final String MEDIAVIDEO_ROUTE = "media/mediavideo"; + public static final String MEDIAARTICLE_ROUTE = "media/mediaarticle"; + public static final String MEDIA_ROLE = "ROLE_MEDIA"; + public static final String MEDIAACTOR_ROUTE = "media/mediaactor"; + public static final String MEDIAACTORFILE_ROUTE = "media/mediaactorfile"; + public static final String MEDIAACTORFILE = "Media Actor Files"; + private static final String MEDIAFILE = "Media Files"; + private static final String MEDIAVIDEO = "Media Videos"; + private static final String MEDIAARTICLE = "Media Article"; + private static final String MEDIAACTOR = "Media Actor"; + + public static SideNavItem getMediaNavigation(ArrayList roles) { + SideNavItem media = new SideNavItem(MEDIA, MEDIAFILE_ROUTE, VaadinIcon.VIMEO.create()); + media.addItem(new SideNavItem(MEDIAVIDEO, MediaVideoView.class)); + media.addItem(new SideNavItem(MEDIAARTICLE, MediaArticleView.class)); + if (roles.contains(MEDIA_ROLE)) { + media.addItem(new SideNavItem(MEDIAFILE, MediaFileView.class)); + media.addItem(new SideNavItem(MEDIAACTOR, MediaActorView.class)); + } + return media; + } + + private MediaConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java new file mode 100644 index 0000000..7c5671c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/SetupModuleMedia.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.admin.services.ModuleService; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleMedia implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private AdminService adminService; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + adminService.addPermission(MediaConstants.MEDIA_ROLE); + if (alreadySetup) { + log.info("SetupModuleMedia already executed, skipping"); + return; + } + if (!moduleService.importData(MediaConstants.MEDIA)) { + log.info("Module media should not setup data"); + return; + } + + log.info("Set up Media data"); + moduleService.setDataImported(MediaConstants.MEDIA); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java new file mode 100644 index 0000000..cfc6270 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActor.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Slf4j +@Entity +public class MediaActor extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_actor", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java new file mode 100644 index 0000000..0e24cd7 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFile.java @@ -0,0 +1,34 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(indexes = {@Index(columnList = "media_file_id, media_actor_id") }, + uniqueConstraints = @UniqueConstraint(columnNames = {"media_file_id", "media_actor_id" }) +) +public class MediaActorFile extends AbstractEntity { + + @ManyToOne + @JoinColumn(name = "media_file_id") + @NotNull + private MediaFile media_file; + + @ManyToOne + @JoinColumn(name = "media_actor_id") + @NotNull + private MediaActor media_actor; + + public String getTitle() { + return media_file.getTitle(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java new file mode 100644 index 0000000..5f364b7 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorFileRepository.java @@ -0,0 +1,6 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MediaActorFileRepository extends JpaRepository { +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java new file mode 100644 index 0000000..973e00a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaActorRepository.java @@ -0,0 +1,17 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaActorRepository extends JpaRepository { + @Query("select m from MediaActor m " + + "where lower(m.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findByNameIgnoreCase(String name); + + MediaActor findByName(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java new file mode 100644 index 0000000..c664a3a --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticle.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@Entity +@Table(indexes = @Index(columnList = "url"), uniqueConstraints = @UniqueConstraint(columnNames = {"url"})) +public class MediaArticle extends AbstractEntity { + + @NotEmpty + private String url; + + private boolean review; + + @Nullable + private String title; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java new file mode 100644 index 0000000..a25702e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaArticleRepository.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaArticleRepository extends JpaRepository { + @Query("select m from MediaArticle m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java new file mode 100644 index 0000000..db1b826 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFile.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.*; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.LinkedList; +import java.util.List; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaFile extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "media_file", cascade = CascadeType.REFRESH, orphanRemoval = true) + @Nullable + List mediaActorFiles = new LinkedList<>(); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java new file mode 100644 index 0000000..7db70e1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaFileRepository.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.media.data; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface MediaFileRepository extends JpaRepository { + @Query("select m from MediaFile m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java new file mode 100644 index 0000000..d093eb1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideo.java @@ -0,0 +1,40 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +@Entity +@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "url" }) }) +public class MediaVideo extends AbstractEntity { + + @Nullable + private String url; + + private boolean review; + + private boolean shouldDownload; + + @Nullable + private String title; + + @Nullable + private String cloudLink; + + @Nullable + private String fileName; + + @Nullable + private String path; + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java new file mode 100644 index 0000000..d898186 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/data/MediaVideoRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.media.data; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface MediaVideoRepository extends JpaRepository { + + @Query("select m from MediaVideo m " + + "where lower(m.url) like lower(concat('%', :searchTerm, '%')) or lower(m.title) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java new file mode 100644 index 0000000..fc78d5d --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaArticleService.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaArticleRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaArticleService { + + private final MediaArticleRepository mediaArticleRepository; + + public MediaArticleService(MediaArticleRepository mediaArticleRepository) { + this.mediaArticleRepository = mediaArticleRepository; + } + + public List findAllMediaArticles(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + log.info("Found {} entries", mediaArticleRepository.count()); + return mediaArticleRepository.findAll(); + } else { + List results = mediaArticleRepository.search(stringFilter); + log.info("Found {} entries", results.size()); + return results; + } + } + + public void saveMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + log.warn("MediaArticle is null. Are you sure you have connected your form to the application?"); + return; + } + mediaArticleRepository.save(mediaArticle); + } + + public void deleteMediaArticle(MediaArticle mediaArticle) { + mediaArticleRepository.delete(mediaArticle); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java new file mode 100644 index 0000000..20e6354 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaFileService.java @@ -0,0 +1,90 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaFileService { + + private final MediaFileRepository mediaFileRepository; + private final MediaActorRepository mediaActorRepository; + private final MediaActorFileRepository mediaActorFileRepository; + + public MediaFileService(MediaFileRepository mediaFileRepository, MediaActorRepository mediaActorRepository, MediaActorFileRepository mediaActorFileRepository) { + this.mediaFileRepository = mediaFileRepository; + this.mediaActorRepository = mediaActorRepository; + this.mediaActorFileRepository = mediaActorFileRepository; + } + + public List findAllMediaFiles(String stringFilter) { + List results; + if (stringFilter == null || stringFilter.isEmpty()) { + results = mediaFileRepository.findAll(); + } else { + results = mediaFileRepository.search(stringFilter); + } + log.debug("Found " + results.size() + " entries"); + return results; + } + + public void saveMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaFileRepository.save(mediaFile); + } + + public void deleteMediaFile(MediaFile mediaFile) { + mediaFileRepository.delete(mediaFile); + } + + public List findAllMediaActors(String stringFilter) { + List results; + if (stringFilter == null || stringFilter.isEmpty()) { + results = mediaActorRepository.findAll(); + } else { + results = mediaActorRepository.search(stringFilter); + } + log.debug("Found " + results.size() + " entries"); + return results; + } + + public void saveMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + log.warn("MediaActor is null. Are you sure you have connected your form to the application?"); + return; + } + mediaActorRepository.save(mediaActor); + } + + public void deleteMediaActor(MediaActor mediaActor) { + mediaActorRepository.delete(mediaActor); + } + + public List findAllMediaActorFiles() { + return mediaActorFileRepository.findAll(); + } + + public void saveMediaActorFile(MediaActorFile mediaActorFile) { + if (mediaActorFile == null){ + log.warn("MediaActorFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaActorFileRepository.save(mediaActorFile); + } + + public void deleteMediaActorFile(MediaActorFile mediaActorFile) { + MediaFile mediaFile = mediaActorFile.getMedia_file(); + mediaFile.getMediaActorFiles().remove(mediaActorFile); + mediaFileRepository.save(mediaFile); + MediaActor mediaActor = mediaActorFile.getMedia_actor(); + mediaActor.getMediaActorFiles().remove(mediaActorFile); + mediaActorRepository.save(mediaActor); + mediaActorFileRepository.delete(mediaActorFile); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java new file mode 100644 index 0000000..61299fd --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/services/MediaVideoService.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.data.MediaVideoRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +public class MediaVideoService { + + private final MediaVideoRepository mediaVideoRepository; + + public MediaVideoService(MediaVideoRepository mediaVideoRepository) { + this.mediaVideoRepository = mediaVideoRepository; + } + + public List findAllMediaVideos(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return mediaVideoRepository.findAll(); + } else { + return mediaVideoRepository.search(stringFilter); + } + } + + public void saveMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + log.warn("MediaFile is null. Are you sure you have connected your form to the application?"); + return; + } + mediaVideoRepository.save(mediaVideo); + } + + public void deleteMediaVideo(MediaVideo mediaVideo) { + mediaVideoRepository.delete(mediaVideo); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java new file mode 100644 index 0000000..ac3acb1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileForm.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.data.MediaActorFile; +import de.thpeetz.kontor.media.data.MediaFile; +import lombok.Getter; + +import java.util.List; + +public class MediaActorFileForm extends FormLayout { + ComboBox mediaFile = new ComboBox<>("Media File"); + ComboBox mediaActor = new ComboBox<>("Actor"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaActorFile.class); + + public MediaActorFileForm(List mediaFiles, List actors) { + addClassName("mediaactorfile-form"); + binder.bindInstanceFields(this); + + mediaFile.setItems(mediaFiles); + mediaFile.setItemLabelGenerator(MediaFile::getTitle); + mediaActor.setItems(actors); + mediaActor.setItemLabelGenerator(MediaActor::getName); + add(mediaFile, mediaActor, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaActorFileForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaActorFileForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaActorFileForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaActorFile(MediaActorFile mediaActorFile) { + binder.setBean(mediaActorFile); + } + + public abstract static class MediaActorFileFormEvent extends ComponentEvent { + @Getter + private MediaActorFile mediaActorFile; + + protected MediaActorFileFormEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, false); + this.mediaActorFile = mediaActorFile; + } + } + + public static class SaveEvent extends MediaActorFileForm.MediaActorFileFormEvent { + SaveEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, mediaActorFile); + } + } + + public static class DeleteEvent extends MediaActorFileForm.MediaActorFileFormEvent { + DeleteEvent(MediaActorFileForm source, MediaActorFile mediaActorFile) { + super(source, mediaActorFile); + } + } + + public static class CloseEvent extends MediaActorFileForm.MediaActorFileFormEvent { + CloseEvent(MediaActorFileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaActorFileForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java new file mode 100644 index 0000000..f1462b6 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorFileView.java @@ -0,0 +1,114 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaActorFile; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAACTORFILE_ROUTE, layout = MainLayout.class) +@PageTitle("MediaActorFile | Media | Kontor") +public class MediaActorFileView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaActorFile.class); + @Getter + MediaActorFileForm form; + MediaFileService service; + + public MediaActorFileView(MediaFileService service) { + this.service = service; + addClassName("mediaactorfile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediaactorfile-grid"); + grid.setSizeFull(); + grid.setColumns("id", "media_actor.name", "media_file.title"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMediaActorFile(event.getValue())); + } + + private void configureForm() { + form = new MediaActorFileForm(service.findAllMediaFiles(null), service.findAllMediaActors(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaActorFile); + form.addDeleteListener(this::deleteMediaActorFile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaActorFile(MediaActorFileForm.SaveEvent event) { + service.saveMediaActorFile(event.getMediaActorFile()); + updateList(); + closeEditor(); + } + + private void deleteMediaActorFile(MediaActorFileForm.DeleteEvent event) { + service.deleteMediaActorFile(event.getMediaActorFile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addMediaActorFileButton = new Button("Add MediaActorFile"); + addMediaActorFileButton.addClickListener(click -> addMediaActorFile()); + + HorizontalLayout toolbar = new HorizontalLayout(addMediaActorFileButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaActorFile(MediaActorFile mediaActorFile) { + if (mediaActorFile == null) { + closeEditor(); + } else { + form.setMediaActorFile(mediaActorFile); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaActorFile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaActorFile() { + grid.asSingleSelect().clear(); + editMediaActorFile(new MediaActorFile()); + } + + public void updateList() { + grid.setItems(service.findAllMediaActorFiles()); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java new file mode 100644 index 0000000..e011fb7 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.listbox.MultiSelectListBox; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.data.MediaActorFile; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class MediaActorForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid mediaActorFiles = new Grid<>(MediaActorFile.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaActor.class); + + public MediaActorForm() { + addClassName("media-actor-form"); + binder.bindInstanceFields(this); + + mediaActorFiles.setColumns("media_file.title"); + mediaActorFiles.getColumnByKey("media_file.title").setHeader("File Title"); + add(name, 2); + add(mediaActorFiles, 2); + add(createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaActorForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaActorForm.CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaActorForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaActor(MediaActor mediaActor) { + binder.setBean(mediaActor); + } + + public void setMediaActorFiles(List mediaActorFiles) { + log.info("Setting mediaActorFiles: {}", mediaActorFiles); + this.mediaActorFiles.setItems(mediaActorFiles); + } + + public abstract static class MediaActorFormEvent extends ComponentEvent { + private MediaActor mediaActor; + + protected MediaActorFormEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, false); + this.mediaActor = mediaActor; + } + + public MediaActor getMediaActor() { + return mediaActor; + } + } + + public static class SaveEvent extends MediaActorForm.MediaActorFormEvent { + SaveEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class DeleteEvent extends MediaActorForm.MediaActorFormEvent { + DeleteEvent(MediaActorForm source, MediaActor mediaActor) { + super(source, mediaActor); + } + } + + public static class CloseEvent extends MediaActorForm.MediaActorFormEvent { + CloseEvent(MediaActorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaActorForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaActorForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaActorForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java new file mode 100644 index 0000000..23ca35e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaActorView.java @@ -0,0 +1,123 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaActor; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAACTOR_ROUTE, layout = MainLayout.class) +@PageTitle("Actor | Media | Kontor") +public class MediaActorView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaActor.class); + TextField filterText = new TextField(); + @Getter + MediaActorForm form; + MediaFileService service; + + public MediaActorView(MediaFileService service) { + this.service = service; + addClassName("media-actor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("media-actor-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editMediaActor(event.getValue())); + } + + private void configureForm() { + form = new MediaActorForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaActor); + form.addDeleteListener(this::deleteMediaActor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaActor(MediaActorForm.SaveEvent event) { + service.saveMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private void deleteMediaActor(MediaActorForm.DeleteEvent event) { + service.deleteMediaActor(event.getMediaActor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addMediaActorButton = new Button("Add actor"); + addMediaActorButton.addClickListener(click -> addMediaActor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addMediaActorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaActor(MediaActor mediaActor) { + if (mediaActor == null) { + closeEditor(); + } else { + form.setMediaActor(mediaActor); + form.setMediaActorFiles(mediaActor.getMediaActorFiles()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaActor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaActor() { + grid.asSingleSelect().clear(); + editMediaActor(new MediaActor()); + } + + public void updateList() { + grid.setItems(service.findAllMediaActors(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java new file mode 100644 index 0000000..d658e4e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleForm.java @@ -0,0 +1,108 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaArticleForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + Checkbox review = new Checkbox("Review"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaArticle.class); + + public MediaArticleForm() { + addClassName("mediaarticle-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(review, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new MediaArticleForm.DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new MediaArticleForm.CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new MediaArticleForm.SaveEvent(this, binder.getBean())); + } + } + + public void setMediaArticle(MediaArticle mediaArticle) { + binder.setBean(mediaArticle); + } + + public abstract static class MediaArticleFormEvent extends ComponentEvent { + private MediaArticle mediaArticle; + + protected MediaArticleFormEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, false); + this.mediaArticle = mediaArticle; + } + + public MediaArticle getMediaArticle() { + return mediaArticle; + } + } + + public static class SaveEvent extends MediaArticleForm.MediaArticleFormEvent { + SaveEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class DeleteEvent extends MediaArticleForm.MediaArticleFormEvent { + DeleteEvent(MediaArticleForm source, MediaArticle mediaArticle) { + super(source, mediaArticle); + } + } + + public static class CloseEvent extends MediaArticleForm.MediaArticleFormEvent { + CloseEvent(MediaArticleForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(MediaArticleForm.DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(MediaArticleForm.SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(MediaArticleForm.CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java new file mode 100644 index 0000000..7728890 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaArticleView.java @@ -0,0 +1,144 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.StatusIcon; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.services.MediaArticleService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAARTICLE_ROUTE, layout = MainLayout.class) +@PageTitle("MediaArticle | Media | Kontor") +public class MediaArticleView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaArticle.class, false); + Grid.Column idColumn = grid.addColumn(MediaArticle::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaArticle::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaArticle::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaArticle -> StatusIcon.create(mediaArticle.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaArticleForm form; + MediaArticleService service; + + public MediaArticleView(MediaArticleService service) { + this.service = service; + addClassName("mediaarticle-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediaarticle-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaArticle(event.getValue())); + } + + private void configureForm() { + form = new MediaArticleForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaArticle); + form.addDeleteListener(this::deleteMediaArticle); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaArticle(MediaArticleForm.SaveEvent event) { + service.saveMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private void deleteMediaArticle(MediaArticleForm.DeleteEvent event) { + service.deleteMediaArticle(event.getMediaArticle()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaArticleButton = new Button("Add MediaArticle"); + addMediaArticleButton.addClickListener(click -> addMediaArticle()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(urlColumn); + columnToggleContextMenu.addColumnToggleItem(titleColumn); + columnToggleContextMenu.addColumnToggleItem(reviewColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaArticleButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaArticle(MediaArticle mediaArticle) { + if (mediaArticle == null) { + closeEditor(); + } else { + form.setMediaArticle(mediaArticle); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaArticle(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaArticle() { + grid.asSingleSelect().clear(); + editMediaArticle(new MediaArticle()); + } + + public void updateList() { + grid.setItems(service.findAllMediaArticles(searchField.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java new file mode 100644 index 0000000..4b4b469 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileForm.java @@ -0,0 +1,128 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaActorFile; +import de.thpeetz.kontor.media.data.MediaFile; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +@Slf4j +public class MediaFileForm extends FormLayout { + + TextField id = new TextField("ID"); + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + Grid mediaActorFiles = new Grid<>(MediaActorFile.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaFile.class); + + public MediaFileForm() { + addClassName("mediafile-form"); + binder.bindInstanceFields(this); + id.setReadOnly(true); + + mediaActorFiles.setColumns("media_actor.name"); + mediaActorFiles.getColumnByKey("media_actor.name").setHeader("Actor"); + + add(id, 2); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload); + add(mediaActorFiles, 2); + add(createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> save.setEnabled(event.getBinder().isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaFile(MediaFile mediaFile) { + binder.setBean(mediaFile); + } + + public void setMediaActorFiles(List actorFiles) { + mediaActorFiles.setItems(actorFiles); + } + + @Getter + public abstract static class MediaFileFormEvent extends ComponentEvent { + private final MediaFile mediaFile; + + protected MediaFileFormEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, false); + this.mediaFile = mediaFile; + } + + } + + public static class SaveEvent extends MediaFileFormEvent { + SaveEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class DeleteEvent extends MediaFileFormEvent { + DeleteEvent(MediaFileForm source, MediaFile mediaFile) { + super(source, mediaFile); + } + } + + public static class CloseEvent extends MediaFileFormEvent { + CloseEvent(MediaFileForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java new file mode 100644 index 0000000..a5c182c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaFileView.java @@ -0,0 +1,167 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.StatusIcon; +import de.thpeetz.kontor.media.data.MediaFile; +import de.thpeetz.kontor.media.services.MediaFileService; +import jakarta.annotation.security.RolesAllowed; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; + +@Slf4j +@SpringComponent +@Scope("prototype") +@RolesAllowed("MEDIA") +@Route(value = "media/mediafile", layout = MainLayout.class) +@PageTitle("MediaFile | Media | Kontor") +public class MediaFileView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaFile.class, false); + Grid.Column idColumn = grid.addColumn(MediaFile::getId) + .setHeader("ID").setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(MediaFile::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(MediaFile::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column urlColumn = grid.addColumn(MediaFile::getUrl) + .setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaFile::getTitle) + .setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaFile::getFileName) + .setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaFile::getCloudLink) + .setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediafile -> StatusIcon.create(mediafile.isReview())). + setHeader("Überprüfung").setWidth("6rem").setSortable(true); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediafile -> StatusIcon.create(mediafile.isShouldDownload())). + setHeader("Download?").setWidth("6rem").setSortable(true); + TextField searchField = new TextField(); + @Getter + MediaFileForm form; + MediaFileService service; + + public MediaFileView(MediaFileService service) { + this.service = service; + addClassName("mediafile-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediafile-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + //idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaFile(event.getValue())); + } + + private void configureForm() { + form = new MediaFileForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaFile); + form.addDeleteListener(this::deleteMediaFile); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaFile(MediaFileForm.SaveEvent event) { + service.saveMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private void deleteMediaFile(MediaFileForm.DeleteEvent event) { + service.deleteMediaFile(event.getMediaFile()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaFileButton = new Button("Add MediaFile"); + addMediaFileButton.addClickListener(click -> addMediaFile()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(createdColumn); + columnToggleContextMenu.addColumnToggleItem(modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(urlColumn); + columnToggleContextMenu.addColumnToggleItem(titleColumn); + columnToggleContextMenu.addColumnToggleItem(fileNameColumn); + columnToggleContextMenu.addColumnToggleItem(cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem(reviewColumn); + columnToggleContextMenu.addColumnToggleItem(shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaFileButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaFile(MediaFile mediaFile) { + if (mediaFile == null) { + closeEditor(); + } else { + form.setMediaFile(mediaFile); + if (mediaFile.getMediaActorFiles() == null) { + log.info("no MediaActorFiles"); + } else { + log.info("MediaActorFiles size: {}", mediaFile.getMediaActorFiles().size()); + } + form.setMediaActorFiles(mediaFile.getMediaActorFiles()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaFile(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaFile() { + grid.asSingleSelect().clear(); + editMediaFile(new MediaFile()); + } + + public void updateList() { + grid.setItems(service.findAllMediaFiles(searchField.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java new file mode 100644 index 0000000..dbc8d47 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoForm.java @@ -0,0 +1,113 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.Checkbox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MediaVideoForm extends FormLayout { + + TextField url = new TextField("URL"); + TextField title = new TextField("Title"); + TextField fileName = new TextField("Dateiname"); + TextField cloudLink = new TextField("Cloud Link"); + Checkbox review = new Checkbox("Review"); + Checkbox shouldDownload = new Checkbox("Download"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(MediaVideo.class); + + public MediaVideoForm() { + addClassName("mediavideo-form"); + binder.bindInstanceFields(this); + add(url, 2); + add(title, 2); + add(fileName, 2); + add(cloudLink, 2); + add(review, shouldDownload, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(event -> { + save.setEnabled(event.getBinder().isValid()); + }); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setMediaVideo(MediaVideo mediaVideo) { + binder.setBean(mediaVideo); + } + + public abstract static class MediaVideoFormEvent extends ComponentEvent { + private MediaVideo mediaVideo; + + protected MediaVideoFormEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, false); + this.mediaVideo = mediaVideo; + } + + public MediaVideo getMediaVideo() { + return mediaVideo; + } + } + + public static class SaveEvent extends MediaVideoFormEvent { + SaveEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class DeleteEvent extends MediaVideoFormEvent { + DeleteEvent(MediaVideoForm source, MediaVideo mediaVideo) { + super(source, mediaVideo); + } + } + + public static class CloseEvent extends MediaVideoFormEvent { + CloseEvent(MediaVideoForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java new file mode 100644 index 0000000..37f5554 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/media/views/MediaVideoView.java @@ -0,0 +1,151 @@ +package de.thpeetz.kontor.media.views; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.contextmenu.ContextMenu; +import com.vaadin.flow.component.contextmenu.MenuItem; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.StatusIcon; +import de.thpeetz.kontor.media.MediaConstants; +import de.thpeetz.kontor.media.data.MediaVideo; +import de.thpeetz.kontor.media.services.MediaVideoService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; +import org.springframework.context.annotation.Scope; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = MediaConstants.MEDIAVIDEO_ROUTE, layout = MainLayout.class) +@PageTitle("MediaVideo | Media | Kontor") +public class MediaVideoView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(MediaVideo.class, false); + Grid.Column idColumn = grid.addColumn(MediaVideo::getId).setHeader("ID").setResizable(true); + Grid.Column urlColumn = grid.addColumn(MediaVideo::getUrl).setHeader("URL").setResizable(true).setSortable(true); + Grid.Column titleColumn = grid.addColumn(MediaVideo::getTitle).setHeader("Titel").setResizable(true).setSortable(true); + Grid.Column fileNameColumn = grid.addColumn(MediaVideo::getFileName).setHeader("Dateiname").setResizable(true).setSortable(true); + Grid.Column cloudLinkColumn = grid.addColumn(MediaVideo::getCloudLink).setHeader("Cloud Link").setResizable(true).setSortable(true); + Grid.Column reviewColumn = grid.addComponentColumn(mediaVideo -> StatusIcon.create(mediaVideo.isReview())) + .setHeader("Überprüfung") + .setWidth("6rem"); + Grid.Column shouldDownloadColumn = grid.addComponentColumn(mediaVideo -> StatusIcon.create(mediaVideo.isShouldDownload())) + .setHeader("Download?") + .setWidth("6rem"); + TextField searchField = new TextField(); + @Getter + MediaVideoForm form; + MediaVideoService service; + + public MediaVideoView(MediaVideoService service) { + this.service = service; + addClassName("mediavideo-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("mediavideo-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + idColumn.setVisible(false); + grid.asSingleSelect().addValueChangeListener(event -> editMediaVideo(event.getValue())); + } + + private void configureForm() { + form = new MediaVideoForm(); + form.setWidth("75em"); + form.setVisible(false); + form.addSaveListener(this::saveMediaVideo); + form.addDeleteListener(this::deleteMediaVideo); + form.addCloseListener(e -> closeEditor()); + } + + private void saveMediaVideo(MediaVideoForm.SaveEvent event) { + service.saveMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private void deleteMediaVideo(MediaVideoForm.DeleteEvent event) { + service.deleteMediaVideo(event.getMediaVideo()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + searchField.setPlaceholder("Search"); + searchField.setClearButtonVisible(true); + searchField.setPrefixComponent(new Icon(VaadinIcon.SEARCH)); + searchField.setValueChangeMode(ValueChangeMode.EAGER); + searchField.addValueChangeListener(e -> updateList()); + + Button addMediaVideoButton = new Button("Add MediaVideo"); + addMediaVideoButton.addClickListener(click -> addMediaVideo()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(urlColumn); + columnToggleContextMenu.addColumnToggleItem(titleColumn); + columnToggleContextMenu.addColumnToggleItem(fileNameColumn); + columnToggleContextMenu.addColumnToggleItem(cloudLinkColumn); + columnToggleContextMenu.addColumnToggleItem(reviewColumn); + columnToggleContextMenu.addColumnToggleItem(shouldDownloadColumn); + HorizontalLayout toolbar = new HorizontalLayout(searchField, addMediaVideoButton, menuButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editMediaVideo(MediaVideo mediaVideo) { + if (mediaVideo == null) { + closeEditor(); + } else { + form.setMediaVideo(mediaVideo); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setMediaVideo(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addMediaVideo() { + grid.asSingleSelect().clear(); + editMediaVideo(new MediaVideo()); + } + + public void updateList() { + grid.setItems(service.findAllMediaVideos(searchField.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java new file mode 100644 index 0000000..ae77838 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityConfig.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.security; + +import com.vaadin.flow.spring.security.VaadinWebSecurity; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import de.thpeetz.kontor.admin.views.LoginView; + +import java.util.Base64; + +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.jose.jws.JwsAlgorithms; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@EnableWebSecurity +@Configuration +public class SecurityConfig extends VaadinWebSecurity { + + public static final String LOGOUT_URL = "/"; + + @Value("${jwt.auth.secret}") + private String authSecret; + + @Autowired + private KontorUserDetailsService userDetailsService; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeHttpRequests(auth -> auth.requestMatchers( + AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/images/*.png")).permitAll()); + super.configure(http); + setLoginView(http, LoginView.class); + setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(authSecret), JwsAlgorithms.HS256), + "de.thpeetz.kontor"); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(encoder()); + return authProvider; + } + + @Bean + public PasswordEncoder encoder() { + return new BCryptPasswordEncoder(11); + } +} \ No newline at end of file diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityService.java new file mode 100644 index 0000000..4d17d31 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/security/SecurityService.java @@ -0,0 +1,105 @@ +package de.thpeetz.kontor.security; + +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Component; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.server.VaadinServletRequest; +import com.vaadin.flow.server.VaadinServletResponse; +import com.vaadin.flow.spring.security.AuthenticationContext; + +import de.thpeetz.kontor.admin.services.KontorUserDetailsService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Component +@Service +public class SecurityService { + + private static final String LOGOUT_SUCCESS_URL = "/"; + + private final AuthenticationContext authenticationContext; + + @Autowired + private KontorUserDetailsService userService; + + public SecurityService(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + } + + public Optional getAuthenticatedUser() { + String name = SecurityContextHolder.getContext().getAuthentication().getName(); + log.info("getAuthenticatedUser: {}", name); + + return Optional.ofNullable(userService.loadUserByUsername(name)); + } + + /*public static UserDetails getAuthenticatedUser() { + SecurityContext context = SecurityContextHolder.getContext(); + final Authentication authentication = context.getAuthentication(); + log.info("Authentication: {}", authentication); + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof UserDetails) { + return (UserDetails) authentication.getPrincipal(); + } + return null; + }*/ + + public void logout() { + log.info("logout"); + authenticationContext.logout(); + clearCookies(); + + UI.getCurrent().getPage().setLocation(LOGOUT_SUCCESS_URL); + SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler(); + logoutHandler.logout(VaadinServletRequest.getCurrent().getHttpServletRequest(), null, null); + clearCookies(); + } + + public boolean isLoggedIn() { + log.info("User {}", getAuthenticatedUser()); + return getAuthenticatedUser().isPresent(); + } + + private static final String JWT_HEADER_AND_PAYLOAD_COOKIE_NAME = "jwt.headerAndPayload"; + private static final String JWT_SIGNATURE_COOKIE_NAME = "jwt.signature"; + + private void clearCookies() { + log.info("clearCookies"); + clearCookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME); + clearCookie(JWT_SIGNATURE_COOKIE_NAME); + } + + private void clearCookie(String cookieName) { + log.info("clearCookie: {}", cookieName); + HttpServletRequest request = VaadinServletRequest.getCurrent() + .getHttpServletRequest(); + HttpServletResponse response = VaadinServletResponse.getCurrent() + .getHttpServletResponse(); + + Cookie k = new Cookie(cookieName, null); + k.setPath(getRequestContextPath(request)); + k.setMaxAge(0); + k.setSecure(request.isSecure()); + k.setHttpOnly(false); + response.addCookie(k); + } + + private String getRequestContextPath(HttpServletRequest request) { + log.info("getRequestContextPath"); + final String contextPath = request.getContextPath(); + return "".equals(contextPath) ? "/" : contextPath; + } +} \ No newline at end of file diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java new file mode 100644 index 0000000..c4a3a7c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/SetupModuleTysc.java @@ -0,0 +1,493 @@ +package de.thpeetz.kontor.tysc; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.repository.CardRepository; +import de.thpeetz.kontor.tysc.repository.CardSetRepository; +import de.thpeetz.kontor.tysc.repository.FieldPositionRepository; +import de.thpeetz.kontor.tysc.repository.PlayerRepository; +import de.thpeetz.kontor.tysc.repository.RoosterRepository; +import de.thpeetz.kontor.tysc.repository.SportRepository; +import de.thpeetz.kontor.tysc.repository.TeamRepository; +import de.thpeetz.kontor.tysc.repository.VendorRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import de.thpeetz.kontor.admin.services.ModuleService; +import de.thpeetz.kontor.tysc.data.FieldPosition; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class SetupModuleTysc implements ApplicationListener { + + boolean alreadySetup = false; + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private RoosterRepository roosterRepository; + + @Autowired + private CardRepository cardRepository; + + @Autowired + private ModuleService moduleService; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (alreadySetup) { + log.info("SetupDataLoader(TYSC) already executed, skipping"); + return; + } + if (!moduleService.importData(TyscConstants.TYSC)) { + log.info("Module TradeYourSportsCards should not setup data"); + return; + } + log.info("Setp up TYSC data"); + Sport football = createSportIfNotFound("Football"); + Sport baseball = createSportIfNotFound("Baseball"); + Sport basketball = createSportIfNotFound("Basketball"); + Sport hockey = createSportIfNotFound("Hockey"); + createTeamIfNotFound(football, "Buffalo Bills", "Bills"); + Team colts = createTeamIfNotFound(football, "Indianapolis Colts", "Colts"); + createTeamIfNotFound(football, "Miami Dolphins", "Dolphins"); + Team patriots = createTeamIfNotFound(football, "New England Patriots", "Patriots"); + createTeamIfNotFound(football, "New York Jets", "Jets"); + Team ravens = createTeamIfNotFound(football, "Baltimore Ravens", "Ravens"); + createTeamIfNotFound(football, "Cincinnati Bengals", "Bengals"); + Team browns = createTeamIfNotFound(football, "Cleveland Browns", "Browns"); + createTeamIfNotFound(football, "Jacksonville Jaguars", "Jaguars"); + Team steelers = createTeamIfNotFound(football, "Pittsburgh Steelers", "Steelers"); + createTeamIfNotFound(football, "Tennessee Titans", "Titans"); + createTeamIfNotFound(football, "Denver Broncos", "Broncos"); + Team chiefs = createTeamIfNotFound(football, "Kansas City Chiefs", "Chiefs"); + Team raiders = createTeamIfNotFound(football, "Oakland Raiders", "Raiders"); + createTeamIfNotFound(football, "San Diego Chargers", "Chargers"); + Team seahawks = createTeamIfNotFound(football, "Seattle Seahawks", "Seahawks"); + createTeamIfNotFound(football, "Arizona Cardinals", "Cardinals"); + Team cowboys = createTeamIfNotFound(football, "Dallas Cowboys", "Cowboys"); + Team giants = createTeamIfNotFound(football, "New York Giants", "Giants"); + Team eagles = createTeamIfNotFound(football, "Philadelphia Eagles", "Eagles"); + Team redskins = createTeamIfNotFound(football, "Washington Redskins", "Redskins"); + createTeamIfNotFound(football, "Chicago Bears", "Bears"); + createTeamIfNotFound(football, "Detroit Lions", "Lions"); + createTeamIfNotFound(football, "Green Bay Packers", "Packers"); + createTeamIfNotFound(football, "Minnesota Vikings", "Vikings"); + createTeamIfNotFound(football, "Tampa Bay Buccaneers", "Buccaneers"); + Team falcons = createTeamIfNotFound(football, "Atlanta Falcons", "Falcons"); + createTeamIfNotFound(football, "Carolina Panthers", "Panthers"); + Team saints = createTeamIfNotFound(football, "New Orleans Saints", "Saints"); + Team rams = createTeamIfNotFound(football, "St.Louis Rams", "Rams"); + Team sf49ers = createTeamIfNotFound(football, "San Francisco 49ers", "49ers"); + createTeamIfNotFound(football, "Houston Texans", "Texans"); + createTeamIfNotFound(football, "Houston Oilers", "Oilers"); + createTeamIfNotFound(baseball, "Baltimore Orioles", "Orioles"); + createTeamIfNotFound(baseball, "Boston Red Sox", "Red Sox"); + createTeamIfNotFound(baseball, "New York Yankees", "Yankees"); + createTeamIfNotFound(baseball, "Tampa Bay Devil Rays", "Devil Rays"); + createTeamIfNotFound(baseball, "Toronto Blue Jays", "Blue Jays"); + createTeamIfNotFound(baseball, "Chicago White Sox", "White Sox"); + createTeamIfNotFound(baseball, "Cleveland Indians", "Indians"); + createTeamIfNotFound(baseball, "Detroit Tigers", "Tigers"); + createTeamIfNotFound(baseball, "Kansas City Royals", "Royals"); + createTeamIfNotFound(baseball, "Minnesota Twins", "Twins"); + createTeamIfNotFound(baseball, "Anaheim Angels", "Angels"); + createTeamIfNotFound(baseball, "Oakland Athletics", "Athletics"); + createTeamIfNotFound(baseball, "Seattle Mariners", "Mariners"); + createTeamIfNotFound(baseball, "Texas Rangers", "Rangers"); + createTeamIfNotFound(baseball, "Atlanta Braves", "Braves"); + createTeamIfNotFound(baseball, "Florida Marlins", "Marlins"); + createTeamIfNotFound(baseball, "Montreal Expos", "Expos"); + createTeamIfNotFound(baseball, "New York Mets", "Mets"); + createTeamIfNotFound(baseball, "Philadelphia Phillies", "Phillies"); + createTeamIfNotFound(baseball, "Chicago Cubs", "Cubs"); + createTeamIfNotFound(baseball, "Cincinnati Reds", "Reds"); + createTeamIfNotFound(baseball, "Houston Astros", "Astros"); + createTeamIfNotFound(baseball, "Milwaukee Brewers", "Brewers"); + createTeamIfNotFound(baseball, "Pittsburgh Pirates", "Pirates"); + createTeamIfNotFound(baseball, "St.Louis Cardinals", "Cardinals"); + createTeamIfNotFound(baseball, "Arizona Diamondbacks", "Diamondbacks"); + createTeamIfNotFound(baseball, "Colorado Rockies", "Rockies"); + createTeamIfNotFound(baseball, "Los Angeles Dodgers", "Dodgers"); + createTeamIfNotFound(baseball, "San Diego Padres", "Padres"); + createTeamIfNotFound(baseball, "San Francisco Giants", "Giants"); + createTeamIfNotFound(basketball, "Boston Celtics", "Celtics"); + createTeamIfNotFound(basketball, "Miami Heat", "Heat"); + createTeamIfNotFound(basketball, "New Jersey Nets", "Mets"); + createTeamIfNotFound(basketball, "New York Knicks", "Knicks"); + createTeamIfNotFound(basketball, "Orlando Magic", "Magic"); + createTeamIfNotFound(basketball, "Philadelphia 76ers", "76ers"); + createTeamIfNotFound(basketball, "Washington Wizards", "Wizards"); + createTeamIfNotFound(basketball, "Atlanta Hawks", "Hawks"); + createTeamIfNotFound(basketball, "Charlotte Hornets", "Hornets"); + createTeamIfNotFound(basketball, "Chicago Bulls", "Bulls"); + createTeamIfNotFound(basketball, "Cleveland Cavaliers", "Cavaliers"); + createTeamIfNotFound(basketball, "Detroit Pistons", "Pistons"); + createTeamIfNotFound(basketball, "Indiana Pacers", "Pacers"); + createTeamIfNotFound(basketball, "Milwaukee Bucks", "Bucks"); + createTeamIfNotFound(basketball, "Toronto Raptors", "Raptors"); + createTeamIfNotFound(basketball, "Dallas Mavericks", "Mavericks"); + createTeamIfNotFound(basketball, "Denver Nuggets", "Nuggets"); + createTeamIfNotFound(basketball, "Houston Rockets", "Rockets"); + createTeamIfNotFound(basketball, "Minnesota Timberwolves", "Timberwolves"); + createTeamIfNotFound(basketball, "San Antonio Spurs", "Spurs"); + createTeamIfNotFound(basketball, "Utah Jazz", "Jazz"); + createTeamIfNotFound(basketball, "Vancouver Grizzlies", "Grizzlies"); + createTeamIfNotFound(basketball, "Golden State Warriors", "Warriors"); + createTeamIfNotFound(basketball, "Los Angeles Clippers", "Clippers"); + createTeamIfNotFound(basketball, "Los Angeles Lakers", "Lakers"); + createTeamIfNotFound(basketball, "Phoenix Suns", "Suns"); + createTeamIfNotFound(basketball, "Portland Trail Blazers", "Blazers"); + createTeamIfNotFound(basketball, "Sacramento Kings", "Kings"); + createTeamIfNotFound(basketball, "Seattle SuperSonics", "SuperSonics"); + createTeamIfNotFound(hockey, "Boston Bruins", "Bruins"); + createTeamIfNotFound(hockey, "Buffalo Sabres", "Sabres"); + createTeamIfNotFound(hockey, "Montreal Canadiens", "Canadiens"); + createTeamIfNotFound(hockey, "Ottawa Senators", "Senators"); + createTeamIfNotFound(hockey, "Toronto Maple Leafs", "Maple Leafs"); + createTeamIfNotFound(hockey, "New Jersey Devils", "Devils"); + createTeamIfNotFound(hockey, "New York Islanders", "Islanders"); + createTeamIfNotFound(hockey, "New York Rangers", "Rangers"); + createTeamIfNotFound(hockey, "Philadelphia Flyers", "Flyers"); + createTeamIfNotFound(hockey, "Pittsburgh Penguins", "Penguins"); + createTeamIfNotFound(hockey, "Atlanta Trashers", "Trashers"); + createTeamIfNotFound(hockey, "Carolina Hurricanes", "Hurricanes"); + createTeamIfNotFound(hockey, "Florida Panthers", "Panthers"); + createTeamIfNotFound(hockey, "Tampa Bay Lightnings", "Lightnings"); + createTeamIfNotFound(hockey, "Washington Capitals", "Capitals"); + createTeamIfNotFound(hockey, "Chicago Blackhawks", "Blackhawks"); + createTeamIfNotFound(hockey, "Columbus Blue Jackets", "Blue Jackets"); + createTeamIfNotFound(hockey, "Detroit Red Wings", "Red Wings"); + createTeamIfNotFound(hockey, "Nashville Predators", "Predators"); + createTeamIfNotFound(hockey, "St.Louis Blues", "Blues"); + createTeamIfNotFound(hockey, "Calgary Flames", "Flames"); + createTeamIfNotFound(hockey, "Colorado Avalanche", "Avalanche"); + createTeamIfNotFound(hockey, "Edmonton Oilers", "Oilers"); + createTeamIfNotFound(hockey, "Minnesota Wild", "Wild"); + createTeamIfNotFound(hockey, "Vancouver Canucks", "Canucks"); + createTeamIfNotFound(hockey, "Anaheim Mighty Ducks", "Mighty Ducks"); + createTeamIfNotFound(hockey, "Dallas Stars", "Stars"); + createTeamIfNotFound(hockey, "Los Angeles Kings", "Kings"); + createTeamIfNotFound(hockey, "Phoenix Coyotes", "Coyotes"); + createTeamIfNotFound(hockey, "San Jose Sharks", "Sharks"); + FieldPosition qb = createPosition(football, "QB", "Quarterback"); + FieldPosition rb = createPosition(football, "RB", "Running Back"); + FieldPosition wr = createPosition(football, "WR", "Wide Receiver"); + FieldPosition te = createPosition(football, "TE", "Tight End"); + FieldPosition fb = createPosition(football, "FB", "Fullback"); + createPosition(football, "OL", "Offensive Line"); + createPosition(football, "DL", "Defensive Line"); + FieldPosition lb = createPosition(football, "LB", "Linebacker"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "DE", "Defensive End"); + createPosition(football, "K", "Kicker"); + createPosition(football, "P", "Punter"); + createPosition(football, "S", "Safety"); + createPosition(football, "KR", "Kick Returner"); + createPosition(football, "PR", "Punt Returner"); + createPosition(football, "LS", "Long Snapper"); + createPosition(football, "LG", "Left Guard"); + createPosition(football, "RG", "Right Guard"); + createPosition(football, "OF", "Offensive Tackle"); + createPosition(football, "DB", "Defensive Back"); + createPosition(football, "CB", "Cornerback"); + createPosition(football, "DT", "Defensive Tackle"); + createPosition(football, "NT", "Nose Tackle"); + createPosition(football, "OLB", "Outside Linebacker"); + createPosition(football, "ILB", "Inside Linebacker"); + createPosition(football, "SS", "Strong Safety"); + createPosition(baseball, "P", "Pitcher"); + createPosition(baseball, "C", "Catcher"); + createPosition(baseball, "1B", "First Base"); + createPosition(baseball, "2B", "Second Base"); + createPosition(baseball, "3B", "Third Base"); + createPosition(baseball, "SS", "Shortstop"); + createPosition(baseball, "LF", "Left Field"); + createPosition(baseball, "CF", "Center Field"); + createPosition(baseball, "RF", "Right Field"); + createPosition(basketball, "PG", "Point Guard"); + createPosition(basketball, "SG", "Shooting Guard"); + createPosition(basketball, "SF", "Small Forward"); + createPosition(basketball, "PF", "Power Forward"); + createPosition(basketball, "C", "Center"); + createPosition(hockey, "G", "Goalie"); + createPosition(hockey, "D", "Defense"); + createPosition(hockey, "LW", "Left Wing"); + createPosition(hockey, "RW", "Right Wing"); + createPosition(hockey, "C", "Center"); + Vendor pacific = createVendorIfNotFound("Pacific"); + Vendor fleer = createVendorIfNotFound("Fleer"); + Vendor bowman = createVendorIfNotFound("Bowman"); + Vendor leaf = createVendorIfNotFound("Leaf"); + Vendor upperdeck = createVendorIfNotFound("Upper Deck"); + createVendorIfNotFound("Topps"); + createVendorIfNotFound("Donruss"); + createVendorIfNotFound("Score"); + createVendorIfNotFound("Flair"); + createCardSetIfNotFound("Mystique Big Buzz", fleer, false, true); + createCardSetIfNotFound("Mystique Gold", fleer, true, false); + createCardSetIfNotFound("Pacific Copper", pacific, true, false); + createCardSetIfNotFound("Pacific Gold", pacific, true, false); + CardSet pacificbase = createCardSetIfNotFound(pacific.getName(), pacific, false, false); + createCardSetIfNotFound(fleer.getName(), fleer, false, false); + createCardSetIfNotFound(bowman.getName(), bowman, false, false); + createCardSetIfNotFound(leaf.getName(), leaf, false, false); + createCardSetIfNotFound("Ultra", fleer, false, false); + createCardSetIfNotFound("Mystique", fleer, false, false); + createCardSetIfNotFound("Finest Hour", pacific, false, false); + createCardSetIfNotFound("SP", upperdeck, false, false); + createCardSetIfNotFound("SPX", upperdeck, false, false); + createCardSetIfNotFound("SP Authentic", upperdeck, false, false); + createCardSetIfNotFound("Black Diamond", upperdeck, false, false); + Player jeromepathon = createPlayerIfNotFound("Jerome", "Pathon"); + Player bruschi = createPlayerIfNotFound("Tedy", "Bruschi"); + Player couch = createPlayerIfNotFound("Tim", "Couch"); + Player shea = createPlayerIfNotFound("Aaron", "Shea"); + Player jamallewis = createPlayerIfNotFound("Jamal", "Lewis"); + Player jermainelewis = createPlayerIfNotFound("Jermaine", "Lewis"); + Player tonybanks = createPlayerIfNotFound("Tony", "Banks"); + Player chrisfuamatu = createPlayerIfNotFound("Chris", "Fuamatu-Ma'afala"); + Player jeromebettis = createPlayerIfNotFound("Jerome", "Bettis"); + Player kordellstewart = createPlayerIfNotFound("Kordell", "Stewart"); + Player warrenmoon = createPlayerIfNotFound("Warren", "Moon"); + Player kevinlockett = createPlayerIfNotFound("Kevin", "Lockett"); + Player richgannon = createPlayerIfNotFound("Rich", "Gannon"); + Player jamesjett = createPlayerIfNotFound("James", "Jett"); + Player mackstrong = createPlayerIfNotFound("Mack", "Strong"); + Player brockhuard = createPlayerIfNotFound("Brock", "Huard"); + Player rickywatters = createPlayerIfNotFound("Ricky", "Watters"); + Player troyaikman = createPlayerIfNotFound("Troy", "Aikman"); + Player davidlafleur = createPlayerIfNotFound("David", "LaFleur"); + Player chrisbrazzell = createPlayerIfNotFound("Chris", "Brazzell"); + Player rondayne = createPlayerIfNotFound("Ron", "Dayne"); + Player nabrowne = createPlayerIfNotFound("Na", "Brown"); + Player torrancesmall = createPlayerIfNotFound("Torrance", "Small"); + Player chadlewis = createPlayerIfNotFound("Chad", "Lewis"); + Player adrianmurrell = createPlayerIfNotFound("Adrian", "Murrell"); + Player mauricesmith = createPlayerIfNotFound("Maurice", "Smith"); + Player chrischandler = createPlayerIfNotFound("Chris", "Chandler"); + Player dannykenell = createPlayerIfNotFound("Danny", "Kanell"); + Player rickywilliams = createPlayerIfNotFound("Ricky", "Williams"); + Player jeffgarcia = createPlayerIfNotFound("Jeff", "Garcia"); + Player taistreets = createPlayerIfNotFound("Tai", "Streets"); + Player charliegarner = createPlayerIfNotFound("Charlie", "Garner"); + Player drewbledsoe = createPlayerIfNotFound("Drew", "Bledsoe"); + createPlayerIfNotFound("Antowain", "Smith"); + createPlayerIfNotFound("Terry", "Glenn"); + Player jerryrice = createPlayerIfNotFound("Jerry", "Rice"); + Player terrellowens = createPlayerIfNotFound("Terrell", "Owens"); + Player isaacbruce = createPlayerIfNotFound("Isaac", "Bruce"); + Player trungcanidate = createPlayerIfNotFound("Trung", "Canidate"); + Rooster pathoncoltswr2001 = createRoosterIfNotFound(jeromepathon, colts, wr, 2001); + Rooster bruschipatriotslb2001 = createRoosterIfNotFound(bruschi, patriots, lb, 2001); + Rooster couchbrownsqb2001 = createRoosterIfNotFound(couch, browns, qb, 2001); + Rooster sheabrownste2001 = createRoosterIfNotFound(shea, browns, te, 2001); + Rooster jamallewisravensrb2001 = createRoosterIfNotFound(jamallewis, ravens, rb, 2001); + Rooster jermainelewisravenswr2001 = createRoosterIfNotFound(jermainelewis, ravens, wr, 2001); + Rooster tonybanksravensqb2001 = createRoosterIfNotFound(tonybanks, ravens, qb, 2001); + createRoosterIfNotFound(tonybanks, redskins, qb, 2002); + Rooster chrisfuamatusteelersfb2001 = createRoosterIfNotFound(chrisfuamatu, steelers, fb, 2001); + Rooster jeromebettissteelersrb2001 = createRoosterIfNotFound(jeromebettis, steelers, rb, 2001); + Rooster kordellstewartsteelersqb2001 = createRoosterIfNotFound(kordellstewart, steelers, qb, 2001); + Rooster warrenmoonchiefsqb2001 = createRoosterIfNotFound(warrenmoon, chiefs, qb, 2001); + Rooster kevinlockettchiefswr2001 = createRoosterIfNotFound(kevinlockett, chiefs, wr, 2001); + Rooster richgannonqbraidersqb2001 = createRoosterIfNotFound(richgannon, raiders, qb, 2001); + Rooster jamesjettraiderswr2001 = createRoosterIfNotFound(jamesjett, raiders, wr,2001); + Rooster mackstrongseahawksfb2001 = createRoosterIfNotFound(mackstrong, seahawks, fb, 2001); + Rooster brockhuardseahawksqb2001 = createRoosterIfNotFound(brockhuard, seahawks, qb, 2001); + Rooster rickywattersseahawksrb2001 = createRoosterIfNotFound(rickywatters, seahawks, rb, 2001); + Rooster troyaikmancowboysqb2001 = createRoosterIfNotFound(troyaikman, cowboys, qb, 2001); + Rooster davidlafleurcowboyste2001 = createRoosterIfNotFound(davidlafleur, cowboys, te, 2001); + Rooster chrisbrazzellcowboyswr2001 = createRoosterIfNotFound(chrisbrazzell, cowboys, wr, 2001); + Rooster rondaynegiantsrb2001 = createRoosterIfNotFound(rondayne, giants, rb, 2001); + Rooster nabrownegiantswr2001 = createRoosterIfNotFound(nabrowne, giants, wr, 2001); + Rooster torrancesmalleagleswr2001 = createRoosterIfNotFound(torrancesmall, eagles, wr, 2001); + Rooster chadlewiseagleste2001 = createRoosterIfNotFound(chadlewis, eagles, te, 2001); + Rooster adrianmurrellredskinsrb2001 = createRoosterIfNotFound(adrianmurrell, redskins, rb, 2001); + Rooster mauricesmithfalconsrb2001 = createRoosterIfNotFound(mauricesmith, falcons, rb, 2001); + Rooster chrischandlerfalconsqb2001 = createRoosterIfNotFound(chrischandler, falcons, qb, 2001); + Rooster dannykanellfalconsqb2001 = createRoosterIfNotFound(dannykenell, falcons, qb, 2001); + Rooster rickywilliamssaintsrb2001 = createRoosterIfNotFound(rickywilliams, saints, rb, 2001); + Rooster jeffgarcia49ersqb2001 = createRoosterIfNotFound(jeffgarcia, sf49ers, qb, 2001); + Rooster taistreets49erswr2001 = createRoosterIfNotFound(taistreets, sf49ers, wr, 2001); + Rooster charliegarner49ersrb2001 = createRoosterIfNotFound(charliegarner, sf49ers, rb, 2001); + Rooster jerryrice49erswr2001 = createRoosterIfNotFound(jerryrice, sf49ers, wr, 2001); + Rooster terrelowens49erswr2001 = createRoosterIfNotFound(terrellowens, sf49ers, wr, 2001); + Rooster isaacbruseramswr2001 = createRoosterIfNotFound(isaacbruce, rams, wr, 2001); + Rooster trungcanidateramsrb2001 = createRoosterIfNotFound(trungcanidate, rams, rb, 2001); + createCardIfNotFound(185, 2001, pacific, pacificbase, pathoncoltswr2001); + createCardIfNotFound(250, 2001, pacific, pacificbase, bruschipatriotslb2001); + createCardIfNotFound(103, 2001, pacific, pacificbase, couchbrownsqb2001); + createCardIfNotFound(112, 2001, pacific, pacificbase, sheabrownste2001); + createCardIfNotFound(37, 2001, pacific, pacificbase, jamallewisravensrb2001); + createCardIfNotFound(38, 2001, pacific, pacificbase, jermainelewisravenswr2001); + createCardIfNotFound(31, 2001, pacific, pacificbase, tonybanksravensqb2001); + createCardIfNotFound(338, 2001, pacific, pacificbase, chrisfuamatusteelersfb2001); + createCardIfNotFound(335, 2001, pacific, pacificbase, jeromebettissteelersrb2001); + createCardIfNotFound(345, 2001, pacific, pacificbase, kordellstewartsteelersqb2001); + createCardIfNotFound(213, 2001, pacific, pacificbase, warrenmoonchiefsqb2001); + createCardIfNotFound(212, 2001, pacific, pacificbase, kevinlockettchiefswr2001); + createCardIfNotFound(311, 2001, pacific, pacificbase, richgannonqbraidersqb2001); + createCardIfNotFound(312, 2001, pacific, pacificbase, jamesjettraiderswr2001); + createCardIfNotFound(403, 2001, pacific, pacificbase, mackstrongseahawksfb2001); + createCardIfNotFound(397, 2001, pacific, pacificbase, brockhuardseahawksqb2001); + createCardIfNotFound(404, 2001, pacific, pacificbase, rickywattersseahawksrb2001); + createCardIfNotFound(116, 2001, pacific, pacificbase, troyaikmancowboysqb2001); + createCardIfNotFound(122, 2001, pacific, pacificbase, davidlafleurcowboyste2001); + createCardIfNotFound(117, 2001, pacific, pacificbase, chrisbrazzellcowboyswr2001); + createCardIfNotFound(281, 2001, pacific, pacificbase, rondaynegiantsrb2001); + createCardIfNotFound(321, 2001, pacific, pacificbase, nabrownegiantswr2001); + createCardIfNotFound(331, 2001, pacific, pacificbase, torrancesmalleagleswr2001); + createCardIfNotFound(324, 2001, pacific, pacificbase, chadlewiseagleste2001); + createCardIfNotFound(445, 2001, pacific, pacificbase, adrianmurrellredskinsrb2001); + createCardIfNotFound(28, 2001, pacific, pacificbase, mauricesmithfalconsrb2001); + createCardIfNotFound(17, 2001, pacific, pacificbase, chrischandlerfalconsqb2001); + createCardIfNotFound(23, 2001, pacific, pacificbase, dannykanellfalconsqb2001); + createCardIfNotFound(273, 2001, pacific, pacificbase, rickywilliamssaintsrb2001); + createCardIfNotFound(380, 2001, pacific, pacificbase, jeffgarcia49ersqb2001); + createCardIfNotFound(390, 2001, pacific, pacificbase, taistreets49erswr2001); + createCardIfNotFound(381, 2001, pacific, pacificbase, charliegarner49ersrb2001); + createCardIfNotFound(387, 2001, pacific, pacificbase, jerryrice49erswr2001); + createCardIfNotFound(386, 2001, pacific, pacificbase, terrelowens49erswr2001); + createCardIfNotFound(349, 2001, pacific, pacificbase, isaacbruseramswr2001); + createCardIfNotFound(350, 2001, pacific, pacificbase, trungcanidateramsrb2001); + + alreadySetup = true; + moduleService.setDataImported(TyscConstants.TYSC); + } + + private Sport createSportIfNotFound(String sport) { + log.info("createSportIfNotFound: {}", sport); + Sport sportEntity = sportRepository.findByName(sport); + if (sportEntity == null) { + log.info("Sport {} not found, will create it", sport); + sportEntity = new Sport(); + sportEntity.setName(sport); + sportRepository.save(sportEntity); + } + return sportEntity; + } + + private Team createTeamIfNotFound(Sport football, String name, String shortName) { + log.info("createTeamIfNotFound: {}", name); + Team team = teamRepository.findByName(name); + if (team == null) { + log.info("Team {} not found, will create it", name); + team = new Team(); + team.setName(name); + team.setShortName(shortName); + team.setSport(football); + teamRepository.save(team); + } + return team; + } + + private FieldPosition createPosition(Sport sport, String shortName, String name) { + log.info("createPosition: {} for Sport {}", name, sport.getName()); + FieldPosition fieldPosition = fieldPositionRepository.findByShortNameAndSport(shortName, sport); + if (fieldPosition == null) { + log.info("Position {} not found, will create it", name); + fieldPosition = new FieldPosition(); + fieldPosition.setShortName(shortName); + fieldPosition.setName(name); + fieldPosition.setSport(sport); + fieldPositionRepository.save(fieldPosition); + } + return fieldPosition; + } + + private Vendor createVendorIfNotFound(String name) { + log.info("createVendorIfNotFound: {}", name); + Vendor vendor = vendorRepository.findByName(name); + if (vendor == null) { + log.info("Vendor {} not found, will create it", name); + vendor = new Vendor(); + vendor.setName(name); + vendorRepository.save(vendor); + } + return vendor; + } + + private Player createPlayerIfNotFound(String firstName, String lastName) { + log.info("createPlayerIfNotFound: {} {}", firstName, lastName); + Player player = playerRepository.findByFirstNameAndLastName(firstName, lastName); + if (player == null) { + log.info("Player {} {} not found, will create it", firstName, lastName); + player = new Player(); + player.setFirstName(firstName); + player.setLastName(lastName); + playerRepository.save(player); + } + return player; + } + + private CardSet createCardSetIfNotFound(String setname, Vendor vendor, boolean parallelSet, boolean insertSet) { + log.info("createCardSetIfNotFound: {} {}", setname, vendor.getName()); + CardSet cardSet = cardSetRepository.findByNameAndVendor(setname, vendor); + if (cardSet == null) { + log.info("CardType {} not found, will create it", cardSet); + cardSet = new CardSet(); + cardSet.setName(setname); + cardSet.setVendor(vendor); + cardSet.setParallelSet(parallelSet); + cardSet.setInsertSet(insertSet); + cardSetRepository.save(cardSet); + } + return cardSet; + } + + private Rooster createRoosterIfNotFound(Player player, Team team, FieldPosition fieldPosition, int year) { + log.info("createRoosterIfNotFound; {} {} {} {}", player.getFirstName(), player.getLastName(), team.getName(), + year); + Rooster rooster = roosterRepository.findByReferences(player, team, fieldPosition, year); + if (rooster == null) { + log.info("Rooster {} not found, will create it", player); + rooster = new Rooster(); + rooster.setPlayer(player); + rooster.setTeam(team); + rooster.setPosition(fieldPosition); + rooster.setYear(year); + roosterRepository.save(rooster); + } + return rooster; + } + + private void createCardIfNotFound(int cardNumber, int year, Vendor vendor, CardSet cardset, Rooster rooster) { + log.info("createCardIfNotFound: vendor={} cardset={} rooster={} cardNumber={} year={}", vendor, cardset, + rooster, cardNumber, year); + Card card = cardRepository.search(vendor, cardset, rooster, cardNumber, year); + if (card == null) { + card = new Card(); + card.setVendor(vendor); + card.setCardSet(cardset); + card.setRooster(rooster); + card.setCardNumber(cardNumber); + card.setYear(year); + cardRepository.save(card); + } + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java new file mode 100644 index 0000000..cc41c93 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/TyscConstants.java @@ -0,0 +1,88 @@ +package de.thpeetz.kontor.tysc; + +import com.vaadin.flow.component.icon.VaadinIcon; +import com.vaadin.flow.component.sidenav.SideNavItem; +import com.vaadin.flow.router.RouterLink; + +import de.thpeetz.kontor.tysc.views.CardSetView; +import de.thpeetz.kontor.tysc.views.CardView; +import de.thpeetz.kontor.tysc.views.PlayerView; +import de.thpeetz.kontor.tysc.views.PositionView; +import de.thpeetz.kontor.tysc.views.RoosterView; +import de.thpeetz.kontor.tysc.views.SportView; +import de.thpeetz.kontor.tysc.views.TeamView; +import de.thpeetz.kontor.tysc.views.VendorView; + +/** + * The {@code TyscConstants} class contains constant values related to tysc. + */ +public class TyscConstants { + + public static final String TYSC = "TradeYourSportsCards"; + public static final String TYSC_ROLE = "ROLE_TYSC"; + public static final String VENDOR = "Vendor"; + public static final String VENDOR_ROUTE = "tysc/vendor"; + public static final String CARDSET = "Card Set"; + public static final String CARDSET_ROUTE = "tysc/cardset"; + public static final String CARD = "Card"; + public static final String CARD_ROUTE = "tysc/card"; + public static final String SPORT = "Sport"; + public static final String SPORT_ROUTE = "tysc/sport"; + public static final String TEAM = "Team"; + public static final String TEAM_ROUTE = "tysc/team"; + public static final String POSITION = "Position"; + public static final String POSITION_ROUTE = "tysc/position"; + public static final String PLAYER = "Player"; + public static final String PLAYER_ROUTE = "tysc/player"; + public static final String ROOSTER = "Rooster"; + public static final String ROOSTER_ROUTE = "tysc/rooster"; + + public static RouterLink getSportLink() { + return new RouterLink(SPORT, SportView.class); + } + + public static RouterLink getTeamLink() { + return new RouterLink(TEAM, TeamView.class); + } + + public static RouterLink getPlayerLink() { + return new RouterLink(PLAYER, PlayerView.class); + } + + public static RouterLink getPositionLink() { + return new RouterLink(POSITION, PositionView.class); + } + + public static RouterLink getRoosterLink() { + return new RouterLink(ROOSTER, RoosterView.class); + } + + public static RouterLink getVendorLink() { + return new RouterLink(VENDOR, VendorView.class); + } + + public static RouterLink getCardSetLink() { + return new RouterLink(CARDSET, CardSetView.class); + } + + public static RouterLink getCardLink() { + return new RouterLink(CARD, CardView.class); + } + + public static SideNavItem getTyscNavigation() { + SideNavItem tysc = new SideNavItem(TYSC, VENDOR_ROUTE, VaadinIcon.ARCHIVE.create()); + tysc.addItem(new SideNavItem(SPORT, SportView.class)); + tysc.addItem(new SideNavItem(TEAM, TeamView.class)); + tysc.addItem(new SideNavItem(PLAYER, PlayerView.class)); + tysc.addItem(new SideNavItem(POSITION, PositionView.class)); + tysc.addItem(new SideNavItem(ROOSTER, RoosterView.class)); + tysc.addItem(new SideNavItem(CARDSET, CardSetView.class)); + tysc.addItem(new SideNavItem(CARD, CardView.class)); + tysc.addItem(new SideNavItem(VENDOR, VendorView.class)); + return tysc; + } + + private TyscConstants() { + // private constructor to hide the implicit public one + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Card.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Card.java new file mode 100644 index 0000000..8fa3486 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Card.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "cardNumber, year")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "cardNumber", "year", "vendor_id", "cardSet_id" })} +) +public class Card extends AbstractEntity { + + private int cardNumber; + + private int year; + + @ManyToOne + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Vendor vendor; + + @ManyToOne + @JoinColumn(name = "cardSet_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private CardSet cardSet; + + @ManyToOne + @JoinColumn(name = "rooster_id") + @NotNull + @JsonIgnoreProperties({ "cards" }) + private Rooster rooster; + + public String getVendorName() { + return this.vendor.getName(); + } + + public String getCardSetName() { + return this.cardSet.getName(); + } + + public String getPlayerName() { + return this.rooster.getPlayer().getFullName(); + } +} + diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java new file mode 100644 index 0000000..c72188c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/CardSet.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.data; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, vendor_id")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name", "vendor_id" })} +) +public class CardSet extends AbstractEntity { + + @NotEmpty + private String name; + + @ManyToOne() + @JoinColumn(name = "vendor_id") + @NotNull + @JsonIgnoreProperties({ "cardSets" }) + private Vendor vendor; + + private boolean parallelSet = false; + + private boolean insertSet = false; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java new file mode 100644 index 0000000..a806e70 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/FieldPosition.java @@ -0,0 +1,52 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name, sport_id")}, + uniqueConstraints = { + @UniqueConstraint(columnNames = { "name", "sport_id" }), + @UniqueConstraint(columnNames = { "shortName", "sport_id" }) + } +) +public class FieldPosition extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "positions" }) + private Sport sport; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "position") + @Nullable + private List roosters; +} \ No newline at end of file diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Player.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Player.java new file mode 100644 index 0000000..6517959 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Player.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = { + @Index(columnList = "firstName, lastName"), + @Index(columnList = "lastName, firstName") +}, uniqueConstraints = { + @UniqueConstraint(columnNames = { "firstName", "lastName" }) +}) +public class Player extends AbstractEntity { + + @NotNull + private String firstName; + + @NotNull + private String lastName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "player") + @Nullable + private List roosters; + + public String getFullName() { + StringBuilder fullName = new StringBuilder(); + fullName.append(this.lastName); + fullName.append(", "); + fullName.append(this.firstName); + return fullName.toString(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java new file mode 100644 index 0000000..6410b07 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Rooster.java @@ -0,0 +1,51 @@ +package de.thpeetz.kontor.tysc.data; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "team_id, player_id, position_id")}, + uniqueConstraints = {@UniqueConstraint(name = "uniqueRooster", columnNames = {"year", "team_id", "player_id", "position_id"})} +) +public class Rooster extends AbstractEntity { + + private int year; + + @ManyToOne + @JoinColumn(name = "team_id") + @NotNull + private Team team; + + @ManyToOne + @JoinColumn(name = "player_id") + @NotNull + private Player player; + + @ManyToOne + @JoinColumn(name = "position_id") + @NotNull + private FieldPosition position; + + @Override + public String toString() { + return "Rooster{" + + "year=" + year + + ", team=" + team.getName() + + ", player=" + player.getFullName() + + ", position=" + position.getName() + + '}'; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java new file mode 100644 index 0000000..72635e1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Sport.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.LinkedList; +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Sport extends AbstractEntity { + + @NotEmpty + @Column(unique = true) + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List teams = new LinkedList<>(); + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "sport") + @Nullable + private List positions = new LinkedList<>(); + + public String updateName(String value) { + if (!this.getName().equals(value)) { + this.setName(value); + log.info("update name"); + return "updated " + this.getId() + " with " + value; + } + return "no changes for " + this.getId(); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Team.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Team.java new file mode 100644 index 0000000..353da78 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Team.java @@ -0,0 +1,49 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = { "name" })} +) +public class Team extends AbstractEntity { + + @NotEmpty + private String name; + + @NotEmpty + private String shortName; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "team") + @Nullable + private List roosters; + + @ManyToOne + @JoinColumn(name = "sport_id") + @NotNull + @JsonIgnoreProperties({ "teams" }) + private Sport sport; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java new file mode 100644 index 0000000..f405565 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/data/Vendor.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.tysc.data; + +import java.util.List; + +import de.thpeetz.kontor.common.data.AbstractEntity; +import jakarta.annotation.Nullable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Index; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.NotEmpty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * Represents a vendor entity. + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper=false) +@Entity +@Table(indexes = {@Index(columnList = "name")}, + uniqueConstraints = {@UniqueConstraint(columnNames = "name")} +) +public class Vendor extends AbstractEntity { + + @NotEmpty + private String name; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "vendor") + @Nullable + private List cardSets; +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardRepository.java new file mode 100644 index 0000000..09cfc08 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardRepository.java @@ -0,0 +1,21 @@ +package de.thpeetz.kontor.tysc.repository; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardRepository extends JpaRepository { + + @Query("SELECT c FROM Card c WHERE c.vendor = ?1 AND c.cardSet = ?2 and c.rooster = ?3 and c.cardNumber = ?4 and c.year = ?5") + Card search(Vendor vendor, CardSet cardset, Rooster rooster, int cardNumber, int year); + + @Query("select c from Card c " + + "where str(c.cardNumber) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} + diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardSetRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardSetRepository.java new file mode 100644 index 0000000..3bf2901 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/CardSetRepository.java @@ -0,0 +1,20 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Vendor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface CardSetRepository extends JpaRepository { + + List findByName(String name); + + CardSet findByNameAndVendor(String name, Vendor vendor); + + @Query("select c from CardSet c " + + "where lower(c.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepository.java new file mode 100644 index 0000000..b018173 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepository.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Sport; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface FieldPositionRepository extends JpaRepository { + + @Query("select p from FieldPosition p " + + "where lower(p.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + List findBySport(Sport sport); + + FieldPosition findByShortName(String searchTerm); + + List findByShortNameIgnoreCase(String shortName); + + FieldPosition findByShortNameAndSport(String shortName, Sport sport); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/PlayerRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/PlayerRepository.java new file mode 100644 index 0000000..2f978f2 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/PlayerRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Player; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface PlayerRepository extends JpaRepository { + + @Query("select p from Player p " + + "where lower(p.firstName) like lower(concat('%', :searchTerm, '%')) " + + "or lower(p.lastName) like lower(concat('%', :searchTerm, '%'))") + List search(@Param("searchTerm") String searchTerm); + + Player findByFirstNameAndLastName(String firstName, String lastName); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/RoosterRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/RoosterRepository.java new file mode 100644 index 0000000..b707d6e --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/RoosterRepository.java @@ -0,0 +1,15 @@ +package de.thpeetz.kontor.tysc.repository; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface RoosterRepository extends JpaRepository{ + + @Query("SELECT r FROM Rooster r WHERE r.player = ?1 AND r.team = ?2 AND r.position = ?3 AND r.year = ?4") + Rooster findByReferences(Player player, Team team, FieldPosition position, int year); +} + diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/SportRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/SportRepository.java new file mode 100644 index 0000000..3860a68 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/SportRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Sport; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface SportRepository extends JpaRepository { + @Query("select s from Sport s " + + "where lower(s.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Sport findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/TeamRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/TeamRepository.java new file mode 100644 index 0000000..c0aeae8 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/TeamRepository.java @@ -0,0 +1,28 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Team; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +/** + * The repository interface for managing teams. + */ +public interface TeamRepository extends JpaRepository { + + /** + * Searches for teams based on a search term. + * + * @param searchTerm the search term to match against team names + * @return a list of teams matching the search term + */ + @Query("select t from Team t " + + "where lower(t.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Team findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/VendorRepository.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/VendorRepository.java new file mode 100644 index 0000000..3195c3c --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/repository/VendorRepository.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor.tysc.repository; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Vendor; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface VendorRepository extends JpaRepository { + + @Query("select v from Vendor v where lower(v.name) like lower(concat('%', :searchTerm, '%')) ") + List search(@Param("searchTerm") String searchTerm); + + Vendor findByName(String name); + + List findByNameIgnoreCase(String name); +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java new file mode 100644 index 0000000..f91f55f --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/CardService.java @@ -0,0 +1,93 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.CardRepository; +import de.thpeetz.kontor.tysc.repository.CardSetRepository; +import de.thpeetz.kontor.tysc.repository.VendorRepository; +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Vendor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class CardService { + + private VendorRepository vendorRepository; + + private CardSetRepository cardSetRepository; + + private CardRepository cardRepository; + + public CardService(VendorRepository vendorRepository, CardSetRepository cardSetRepository, CardRepository cardRepository) { + this.vendorRepository = vendorRepository; + this.cardSetRepository = cardSetRepository; + this.cardRepository = cardRepository; + } + + public List findAllVendors(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return vendorRepository.findAll(); + } else { + return vendorRepository.search(stringFilter); + } + } + + public void deleteVendor(Vendor vendor) { + vendorRepository.delete(vendor); + } + + public Vendor saveVendor(Vendor vendor) { + if (vendor == null) { + log.warn("Vendor is null. Are you sure you have connected your form to the application?"); + return null; + } + return vendorRepository.save(vendor); + } + + public List findAllCards(String stringFilter) { + log.info("find cards with filter: {}", stringFilter); + if (stringFilter == null || stringFilter.isEmpty()) { + List cards = cardRepository.findAll(); + log.debug("found {} cards", cards.size()); + return cards; + } else { + return cardRepository.search(stringFilter); + } + } + + public void deleteCard(Card card) { + cardRepository.delete(card); + } + + public Card saveCard(Card card) { + if (card == null) { + log.warn("Card is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardRepository.save(card); + } + + public List findAllCardSets(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return cardSetRepository.findAll(); + } else { + return cardSetRepository.search(stringFilter); + } + } + + public CardSet saveCardSet(CardSet cardSet) { + if (cardSet == null) { + log.warn("CardSet is null. Are you sure you have connected your form to the application?"); + return null; + } + return cardSetRepository.save(cardSet); + } + + public void deleteCardSet(CardSet cardSet) { + cardSetRepository.delete(cardSet); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java new file mode 100644 index 0000000..9536ec5 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/services/SportService.java @@ -0,0 +1,195 @@ +package de.thpeetz.kontor.tysc.services; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; + +import de.thpeetz.kontor.admin.data.MetaDataTable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.repository.FieldPositionRepository; +import de.thpeetz.kontor.tysc.repository.PlayerRepository; +import de.thpeetz.kontor.tysc.repository.RoosterRepository; +import de.thpeetz.kontor.tysc.repository.SportRepository; +import de.thpeetz.kontor.tysc.repository.TeamRepository; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class SportService { + + @Autowired + private SportRepository sportRepository; + + @Autowired + private TeamRepository teamRepository; + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private PlayerRepository playerRepository; + + @Autowired + private RoosterRepository roosterRepository; + + public List findAllSports(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return sportRepository.findAll(); + } else { + return sportRepository.search(stringFilter); + } + } + + public void deleteSport(Sport sport) { + sportRepository.delete(sport); + } + + public Sport saveSport(Sport sport) { + if (sport == null) { + log.warn("Sport is null. Are you sure you have connected your form to the application?"); + return null; + } + return sportRepository.save(sport); + } + + public List findAllPlayers(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return playerRepository.findAll(); + } else { + return playerRepository.search(stringFilter); + } + } + + public Player savePlayer(Player player) { + if (player == null) { + log.warn("Player is null. Are you sure you have connected yout form to the application?"); + return null; + } + return playerRepository.save(player); + } + + public void deletePlayer(Player player) { + playerRepository.delete(player); + } + + public List findAllTeams(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return teamRepository.findAll(); + } else { + return teamRepository.search(stringFilter); + } + } + + public void deleteTeam(Team team) { + teamRepository.delete(team); + } + + public void saveTeam(Team team) { + if (team == null) { + log.warn("Team is null. Are you sure you have connected your form to the application?"); + return; + } + teamRepository.save(team); + } + + public List findAllPositions(String stringFilter) { + if (stringFilter == null || stringFilter.isEmpty()) { + return fieldPositionRepository.findAll(); + } else { + return fieldPositionRepository.search(stringFilter); + } + } + + public List findAllPositionsForSport(Sport sport) { + if (sport == null) { + return fieldPositionRepository.findAll(); + } else { + log.info("Find positions for Sport: {}", sport); + return fieldPositionRepository.findBySport(sport); + } + } + + public void savePosition(FieldPosition position) { + if (position == null) { + log.warn("Position is null. Are you sure you have connected your form to the application?"); + return; + } + fieldPositionRepository.save(position); + } + + public void deletePosition(FieldPosition position) { + fieldPositionRepository.delete(position); + } + + public List findAllRoosters() { + return roosterRepository.findAll(); + } + + public Rooster findRoosterByFields(Team team, Player player, FieldPosition position, Integer year) { + return roosterRepository.findByReferences(player, team, position, year); + } + + public void saveRooster(Rooster rooster) { + if (rooster == null) { + log.warn(""); + return; + } + roosterRepository.save(rooster); + } + + public void deleteRooster(Rooster rooster) { + roosterRepository.delete(rooster); + } + + public String importData(Map fields) { + AtomicReference status = new AtomicReference<>("unknown"); + String id = fields.get("id"); + Optional optional = sportRepository.findById(id); + if (optional.isEmpty()) { + log.info(" not found: {} with {}", id, fields); + status.set(id + "not found"); + Sport checkExisting = sportRepository.findByName(fields.get("name")); + if (checkExisting != null) { + log.info("entry already there with different id ({}), will be deleted", checkExisting.getId()); + deleteSport(checkExisting); + } + Sport sport = new Sport(); + sport.setId(id); + sport.setName(fields.get("name")); + sportRepository.save(sport); + } else { + optional.ifPresent( entry -> { + log.info(" found: {}", entry.getName()); + String updateStatus = updateSportFields(entry, fields); + sportRepository.save(entry); + status.set(updateStatus); + }); + } + return status.get(); + } + + private String updateSportFields(Sport sport, Map fields) { + String status = ""; + for (Map.Entry entry : fields.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + switch (key) { + case "id", "created_date", "last_modified_date", "version": + break; + case "table_name": + status += sport.updateName(value); + default: + log.info("field {} is unknown for table {}", key, sport.getClass().getName()); + } + } + return status; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java new file mode 100644 index 0000000..0b23ff6 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardForm extends FormLayout { + + TextField cardNumber = new TextField("CardNumber"); + TextField year = new TextField("Year"); + ComboBox vendor = new ComboBox<>("Vendor"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Card.class); + + public CardForm(List vendors) { + addClassName("card-form"); + binder.bindInstanceFields(this); + + vendor.setItems(vendors); + vendor.setItemLabelGenerator(Vendor::getName); + add(cardNumber, year, vendor, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCard(Card card) { + binder.setBean(card); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + } + + public abstract static class CardFormEvent extends ComponentEvent { + private Card card; + + protected CardFormEvent(CardForm source, Card card) { + super(source, false); + this.card = card; + } + + public Card getCard() { + return card; + } + } + + public static class SaveEvent extends CardFormEvent { + SaveEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class DeleteEvent extends CardFormEvent { + DeleteEvent(CardForm source, Card card) { + super(source, card); + } + } + + public static class CloseEvent extends CardFormEvent { + CloseEvent(CardForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java new file mode 100644 index 0000000..d0e7e89 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CardSetForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(CardSet.class); + + public CardSetForm() { + addClassName("cardSet-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setCardSet(CardSet cardSet) { + binder.setBean(cardSet); + } + + public abstract static class CardSetFormEvent extends ComponentEvent { + private CardSet cardSet; + + protected CardSetFormEvent(CardSetForm source, CardSet cardSet) { + super(source, false); + this.cardSet = cardSet; + } + + public CardSet getCardSet() { + return cardSet; + } + } + + public static class SaveEvent extends CardSetFormEvent { + SaveEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class DeleteEvent extends CardSetFormEvent { + DeleteEvent(CardSetForm source, CardSet cardSet) { + super(source, cardSet); + } + } + + public static class CloseEvent extends CardSetFormEvent { + CloseEvent(CardSetForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java new file mode 100644 index 0000000..1a49896 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardSetView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARDSET_ROUTE, layout = MainLayout.class) +@PageTitle("CardSet | Tysc | Kontor") +public class CardSetView extends VerticalLayout { + + Grid grid = new Grid<>(CardSet.class); + TextField filterText = new TextField(); + CardSetForm form; + CardService service; + + public CardSetView(CardService service) { + this.service = service; + addClassName("cardSet-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("cardSet-grid"); + grid.setSizeFull(); + grid.setColumns("name", "vendor.name", "parallelSet", "insertSet"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCardSet(event.getValue())); + } + + public CardSetForm getForm() { + return form; + } + + private void configureForm() { + form = new CardSetForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCardSet); + form.addDeleteListener(this::deleteCardSet); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCardSet(CardSetForm.SaveEvent event) { + service.saveCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private void deleteCardSet(CardSetForm.DeleteEvent event) { + service.deleteCardSet(event.getCardSet()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardSetButton = new Button("Add cardSet"); + addCardSetButton.addClickListener(click -> addCardSet()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardSetButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCardSet(CardSet cardSet) { + if (cardSet == null) { + closeEditor(); + } else { + form.setCardSet(cardSet); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCardSet(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCardSet() { + grid.asSingleSelect().clear(); + editCardSet(new CardSet()); + } + + public void updateList() { + grid.setItems(service.findAllCardSets(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java new file mode 100644 index 0000000..f5251da --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/CardView.java @@ -0,0 +1,162 @@ +package de.thpeetz.kontor.tysc.views; + +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.common.views.StatusIcon; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.ColumnToggleContextMenu; +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; +import lombok.Getter; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.CARD_ROUTE, layout = MainLayout.class) +@PageTitle("Card | Tysc | Kontor") +public class CardView extends VerticalLayout { + + @Getter + Grid grid = new Grid<>(Card.class, false); + Grid.Column idColumn = grid.addColumn(Card::getId).setHeader("ID") + .setResizable(true).setSortable(true); + Grid.Column createdColumn = grid.addColumn(Card::getCreatedDate) + .setHeader("Erstellt").setResizable(true).setSortable(true); + Grid.Column modifiedColumn = grid.addColumn(Card::getLastModifiedDate) + .setHeader("Geändert").setResizable(true).setSortable(true); + Grid.Column numberColumn = grid.addColumn(Card::getCardNumber) + .setHeader("Nummer").setResizable(true).setSortable(true); + Grid.Column yearColumn = grid.addColumn(Card::getYear) + .setHeader("Jahr").setResizable(true).setSortable(true); + Grid.Column vendorColumn = grid.addColumn(Card::getVendorName) + .setHeader("Hersteller").setResizable(true).setSortable(true); + Grid.Column cardSetColumn = grid.addColumn(Card::getCardSetName) + .setHeader("Cardset").setResizable(true).setSortable(true); + Grid.Column insertSetColumn = grid.addComponentColumn(card -> StatusIcon.create(card.getCardSet().isInsertSet())) + .setHeader("Inserts").setWidth("6rem").setSortable(true); + Grid.Column parallelSetColumn = grid.addComponentColumn(card -> StatusIcon.create(card.getCardSet().isParallelSet())) + .setHeader("Parallels").setWidth("6rem").setSortable(true); + + Grid.Column playerColumn = grid.addColumn(Card::getPlayerName).setHeader("Spieler").setResizable(true).setSortable(true); + TextField filterText = new TextField(); + @Getter + CardForm form; + CardService service; + + public CardView(CardService service) { + this.service = service; + addClassName("card-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + private void configureGrid() { + grid.addClassName("card-grid"); + grid.setSizeFull(); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editCard(event.getValue())); + idColumn.setVisible(false); + createdColumn.setVisible(false); + modifiedColumn.setVisible(false); + } + + private void configureForm() { + form = new CardForm(service.findAllVendors(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveCard); + form.addDeleteListener(this::deleteCard); + form.addCloseListener(e -> closeEditor()); + } + + private void saveCard(CardForm.SaveEvent event) { + service.saveCard(event.getCard()); + updateList(); + closeEditor(); + } + + private void deleteCard(CardForm.DeleteEvent event) { + service.deleteCard(event.getCard()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addCardButton = new Button("Add card"); + addCardButton.addClickListener(click -> addCard()); + + Button menuButton = new Button("Show/Hide Columns"); + menuButton.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + ColumnToggleContextMenu columnToggleContextMenu = new ColumnToggleContextMenu<>(menuButton); + columnToggleContextMenu.addColumnToggleItem(idColumn); + columnToggleContextMenu.addColumnToggleItem(createdColumn); + columnToggleContextMenu.addColumnToggleItem(modifiedColumn); + columnToggleContextMenu.addColumnToggleItem(numberColumn); + columnToggleContextMenu.addColumnToggleItem(yearColumn); + columnToggleContextMenu.addColumnToggleItem(vendorColumn); + columnToggleContextMenu.addColumnToggleItem(cardSetColumn); + columnToggleContextMenu.addColumnToggleItem(playerColumn); + HorizontalLayout toolbar = new HorizontalLayout(filterText, addCardButton, menuButton); + + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editCard(Card card) { + if (card == null) { + closeEditor(); + } else { + form.setCard(card); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setCard(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addCard() { + grid.asSingleSelect().clear(); + editCard(new Card()); + } + + public void updateList() { + grid.setItems(service.findAllCards(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java new file mode 100644 index 0000000..9730103 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerForm.java @@ -0,0 +1,116 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PlayerForm extends FormLayout { + + TextField firstName = new TextField("First Name"); + TextField lastName = new TextField("Last Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Player.class); + + public PlayerForm() { + addClassName("player-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("team.name", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(firstName, lastName, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPlayer(Player player) { + binder.setBean(player); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PlayerFormEvent extends ComponentEvent { + private Player player; + + protected PlayerFormEvent(PlayerForm source, Player player) { + super(source, false); + this.player = player; + } + + public Player getPlayer() { + return player; + } + } + + public static class SaveEvent extends PlayerFormEvent { + SaveEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class DeleteEvent extends PlayerFormEvent { + DeleteEvent(PlayerForm source, Player player) { + super(source, player); + } + } + + public static class CloseEvent extends PlayerFormEvent { + CloseEvent(PlayerForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java new file mode 100644 index 0000000..56d9588 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PlayerView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.PLAYER_ROUTE, layout = MainLayout.class) +@PageTitle("Player | Tysc | Kontor") +public class PlayerView extends VerticalLayout { + + Grid grid = new Grid<>(Player.class); + TextField filterText = new TextField(); + PlayerForm form; + SportService service; + + public PlayerView(SportService service) { + this.service = service; + addClassName("player-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("player-grid"); + grid.setSizeFull(); + grid.setColumns("firstName", "lastName"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPlayer(event.getValue())); + } + + public PlayerForm getForm() { + return form; + } + + private void configureForm() { + form = new PlayerForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePlayer); + form.addDeleteListener(this::deletePlayer); + form.addCloseListener(e -> closeEditor()); + } + + private void savePlayer(PlayerForm.SaveEvent event) { + service.savePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private void deletePlayer(PlayerForm.DeleteEvent event) { + service.deletePlayer(event.getPlayer()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPlayerButton = new Button("Add player"); + addPlayerButton.addClickListener(click -> addPlayer()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPlayerButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPlayer(Player player) { + if (player == null) { + closeEditor(); + } else { + form.setPlayer(player); + form.setRoosters(player.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPlayer(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPlayer() { + grid.asSingleSelect().clear(); + editPlayer(new Player()); + } + + public void updateList() { + grid.setItems(service.findAllPlayers(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java new file mode 100644 index 0000000..f5b49bb --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Rooster; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class PositionForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(FieldPosition.class); + + public PositionForm() { + addClassName("position-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "team.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setPosition(FieldPosition position) { + binder.setBean(position); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class PositionFormEvent extends ComponentEvent { + private FieldPosition position; + + protected PositionFormEvent(PositionForm source, FieldPosition position) { + super(source, false); + this.position = position; + } + + public FieldPosition getPosition() { + return position; + } + } + + public static class SaveEvent extends PositionFormEvent { + SaveEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class DeleteEvent extends PositionFormEvent { + DeleteEvent(PositionForm source, FieldPosition position) { + super(source, position); + } + } + + public static class CloseEvent extends PositionFormEvent { + CloseEvent(PositionForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java new file mode 100644 index 0000000..ad71bf0 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/PositionView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.POSITION_ROUTE, layout = MainLayout.class) +@PageTitle("Position | Tysc | Kontor") +public class PositionView extends VerticalLayout { + + Grid grid = new Grid<>(FieldPosition.class); + TextField filterText = new TextField(); + PositionForm form; + SportService service; + + public PositionView(SportService service) { + this.service = service; + addClassName("position-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("position-grid"); + grid.setSizeFull(); + grid.setColumns("shortName", "name", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editPosition(event.getValue())); + } + + public PositionForm getForm() { + return form; + } + + private void configureForm() { + form = new PositionForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::savePosition); + form.addDeleteListener(this::deletePosition); + form.addCloseListener(e -> closeEditor()); + } + + private void savePosition(PositionForm.SaveEvent event) { + service.savePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private void deletePosition(PositionForm.DeleteEvent event) { + service.deletePosition(event.getPosition()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addPositionButton = new Button("Add position"); + addPositionButton.addClickListener(click -> addPosition()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addPositionButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editPosition(FieldPosition fieldPosition) { + if (fieldPosition == null) { + closeEditor(); + } else { + form.setPosition(fieldPosition); + form.setRoosters(fieldPosition.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setPosition(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addPosition() { + grid.asSingleSelect().clear(); + editPosition(new FieldPosition()); + } + + public void updateList() { + grid.setItems(service.findAllPositions(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java new file mode 100644 index 0000000..5f9d259 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterForm.java @@ -0,0 +1,117 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.combobox.ComboBox; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class RoosterForm extends FormLayout { + IntegerField year = new IntegerField("Year"); + ComboBox team = new ComboBox<>("Team"); + ComboBox player = new ComboBox<>("Player"); + ComboBox position = new ComboBox<>("Position"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Rooster.class); + + public RoosterForm(List teams, List players, List positions) { + addClassName("rooster-form"); + binder.bindInstanceFields(this); + + team.setItems(teams); + team.setItemLabelGenerator(Team::getName); + player.setItems(players); + player.setItemLabelGenerator(Player::getFullName); + position.setItems(positions); + position.setItemLabelGenerator(FieldPosition::getName); + add(year, team, player, position, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setRooster(Rooster rooster) { + binder.setBean(rooster); + } + + public abstract static class RoosterFormEvent extends ComponentEvent { + private Rooster rooster; + + protected RoosterFormEvent(RoosterForm source, Rooster rooster) { + super(source, false); + this.rooster = rooster; + } + + public Rooster getRooster() { + return rooster; + } + } + + public static class SaveEvent extends RoosterFormEvent { + SaveEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class DeleteEvent extends RoosterFormEvent { + DeleteEvent(RoosterForm source, Rooster rooster) { + super(source, rooster); + } + } + + public static class CloseEvent extends RoosterFormEvent { + CloseEvent(RoosterForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java new file mode 100644 index 0000000..92da6eb --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/RoosterView.java @@ -0,0 +1,121 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.ROOSTER_ROUTE, layout = MainLayout.class) +@PageTitle("Rooster | Tysc | Kontor") +public class RoosterView extends VerticalLayout { + + Grid grid = new Grid<>(Rooster.class); + RoosterForm form; + SportService service; + + public RoosterView(SportService service) { + this.service = service; + addClassName("rooster-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("rooster-grid"); + grid.setSizeFull(); + grid.setColumns("year", "team.name", "player.fullName", "position.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editRooster(event.getValue())); + } + + public RoosterForm getForm() { + return form; + } + + private void configureForm() { + form = new RoosterForm(service.findAllTeams(null), service.findAllPlayers(null), service.findAllPositions(null)); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveRooster); + form.addDeleteListener(this::deleteRooster); + form.addCloseListener(e -> closeEditor()); + } + + private void saveRooster(RoosterForm.SaveEvent event) { + service.saveRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private void deleteRooster(RoosterForm.DeleteEvent event) { + service.deleteRooster(event.getRooster()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + Button addRoosterButton = new Button("Add Rooster"); + addRoosterButton.addClickListener(click -> addRooster()); + + HorizontalLayout toolbar = new HorizontalLayout(addRoosterButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editRooster(Rooster rooster) { + if (rooster == null) { + closeEditor(); + } else { + form.setRooster(rooster); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setRooster(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addRooster() { + grid.asSingleSelect().clear(); + editRooster(new Rooster()); + } + + public void updateList() { + grid.setItems(service.findAllRoosters()); + } +} \ No newline at end of file diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java new file mode 100644 index 0000000..30582e1 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportForm.java @@ -0,0 +1,103 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Sport; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SportForm extends FormLayout { + + TextField name = new TextField("Name"); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Sport.class); + + public SportForm() { + addClassName("sport-form"); + binder.bindInstanceFields(this); + + add(name, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setSport(Sport sport) { + binder.setBean(sport); + } + + public abstract static class SportFormEvent extends ComponentEvent { + private Sport sport; + + protected SportFormEvent(SportForm source, Sport sport) { + super(source, false); + this.sport = sport; + } + + public Sport getSport() { + return sport; + } + } + + public static class SaveEvent extends SportFormEvent { + SaveEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class DeleteEvent extends SportFormEvent { + DeleteEvent(SportForm source, Sport sport) { + super(source, sport); + } + } + + public static class CloseEvent extends SportFormEvent { + CloseEvent(SportForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java new file mode 100644 index 0000000..c6008c3 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/SportView.java @@ -0,0 +1,129 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.SPORT_ROUTE, layout = MainLayout.class) +@PageTitle("Sport | Tysc | Kontor") +public class SportView extends VerticalLayout { + + Grid grid = new Grid<>(Sport.class); + TextField filterText = new TextField(); + SportForm form; + SportService service; + + public SportView(SportService service) { + this.service = service; + addClassName("sport-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("sport-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editSport(event.getValue())); + } + + public SportForm getForm() { + return form; + } + + private void configureForm() { + form = new SportForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveSport); + form.addDeleteListener(this::deleteSport); + form.addCloseListener(e -> closeEditor()); + } + + private void saveSport(SportForm.SaveEvent event) { + service.saveSport(event.getSport()); + updateList(); + closeEditor(); + } + + private void deleteSport(SportForm.DeleteEvent event) { + service.deleteSport(event.getSport()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addSportButton = new Button("Add sport"); + addSportButton.addClickListener(click -> addSport()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addSportButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editSport(Sport sport) { + if (sport == null) { + closeEditor(); + } else { + form.setSport(sport); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setSport(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addSport() { + grid.asSingleSelect().clear(); + editSport(new Sport()); + } + + public void updateList() { + grid.setItems(service.findAllSports(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java new file mode 100644 index 0000000..63cb56b --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TeamForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid roosters = new Grid<>(Rooster.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Team.class); + + public TeamForm() { + addClassName("team-form"); + binder.bindInstanceFields(this); + + roosters.setColumns("player.fullName", "position.name", "year"); + roosters.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, roosters, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setTeam(Team team) { + binder.setBean(team); + } + + public void setRoosters(List roosters) { + log.info("Setting roosters: {}", roosters); + this.roosters.setItems(roosters); + } + + public abstract static class TeamFormEvent extends ComponentEvent { + private Team team; + + protected TeamFormEvent(TeamForm source, Team team) { + super(source, false); + this.team = team; + } + + public Team getTeam() { + return team; + } + } + + public static class SaveEvent extends TeamFormEvent { + SaveEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class DeleteEvent extends TeamFormEvent { + DeleteEvent(TeamForm source, Team team) { + super(source, team); + } + } + + public static class CloseEvent extends TeamFormEvent { + CloseEvent(TeamForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java new file mode 100644 index 0000000..1e48f98 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TeamView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Team; +import de.thpeetz.kontor.tysc.services.SportService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.TEAM_ROUTE, layout = MainLayout.class) +@PageTitle("Team | Tysc | Kontor") +public class TeamView extends VerticalLayout { + + Grid grid = new Grid<>(Team.class); + TextField filterText = new TextField(); + TeamForm form; + SportService service; + + public TeamView(SportService service) { + this.service = service; + addClassName("team-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("team-grid"); + grid.setSizeFull(); + grid.setColumns("name", "shortName", "sport.name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editTeam(event.getValue())); + } + + public TeamForm getForm() { + return form; + } + + private void configureForm() { + form = new TeamForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveTeam); + form.addDeleteListener(this::deleteTeam); + form.addCloseListener(e -> closeEditor()); + } + + private void saveTeam(TeamForm.SaveEvent event) { + service.saveTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private void deleteTeam(TeamForm.DeleteEvent event) { + service.deleteTeam(event.getTeam()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addTeamButton = new Button("Add team"); + addTeamButton.addClickListener(click -> addTeam()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addTeamButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editTeam(Team team) { + if (team == null) { + closeEditor(); + } else { + form.setTeam(team); + form.setRoosters(team.getRoosters()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setTeam(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addTeam() { + grid.asSingleSelect().clear(); + editTeam(new Team()); + } + + public void updateList() { + grid.setItems(service.findAllTeams(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java new file mode 100644 index 0000000..66fb09b --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/TyscLayout.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.views; + +import com.vaadin.flow.component.applayout.AppLayout; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.theme.lumo.LumoUtility; + +import de.thpeetz.kontor.admin.services.AdminService; +import de.thpeetz.kontor.common.views.KontorLayoutUtil; +import de.thpeetz.kontor.security.SecurityService; +import de.thpeetz.kontor.tysc.TyscConstants; +import lombok.extern.slf4j.Slf4j; + +/** + * Represents a custom layout for the comic view in the application. + * This layout extends the AppLayout class. + */ +@Slf4j +public class TyscLayout extends AppLayout { + + private final AdminService adminService; + + private final SecurityService securityService; + + public TyscLayout(AdminService adminService, SecurityService securityService) { + this.adminService = adminService; + this.securityService = securityService; + + KontorLayoutUtil layout = new KontorLayoutUtil(this, adminService, securityService); + layout.setSecondaryNavigation(getSecondaryNavigation()); + layout.createHeader(TyscConstants.TYSC); + } + + private HorizontalLayout getSecondaryNavigation() { + HorizontalLayout navigation = new HorizontalLayout(); + navigation.addClassNames(LumoUtility.JustifyContent.CENTER, LumoUtility.Gap.SMALL, LumoUtility.Height.MEDIUM); + navigation.add(TyscConstants.getSportLink(), TyscConstants.getTeamLink(), TyscConstants.getPositionLink(), + TyscConstants.getPlayerLink(), TyscConstants.getRoosterLink(), TyscConstants.getVendorLink(), + TyscConstants.getCardSetLink(), TyscConstants.getCardLink()); + return navigation; + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java new file mode 100644 index 0000000..254b1ff --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorForm.java @@ -0,0 +1,115 @@ +package de.thpeetz.kontor.tysc.views; + +import java.util.List; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.Key; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.formlayout.FormLayout; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import com.vaadin.flow.data.binder.Binder; + +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.data.CardSet; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class VendorForm extends FormLayout { + + TextField name = new TextField("Name"); + Grid cardSets = new Grid<>(CardSet.class); + + Button save = new Button("Save"); + Button delete = new Button("Delete"); + Button close = new Button("Cancel"); + + Binder binder = new BeanValidationBinder<>(Vendor.class); + + public VendorForm() { + addClassName("vendor-form"); + binder.bindInstanceFields(this); + + cardSets.setColumns("name", "parallelSet"); + cardSets.getColumns().forEach(col -> col.setAutoWidth(true)); + add(name, cardSets, createButtonsLayout()); + } + + private HorizontalLayout createButtonsLayout() { + save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); + delete.addThemeVariants(ButtonVariant.LUMO_ERROR); + close.addThemeVariants(ButtonVariant.LUMO_TERTIARY); + + save.addClickShortcut(Key.ENTER); + close.addClickShortcut(Key.ESCAPE); + + save.addClickListener(event -> validateAndSave()); + delete.addClickListener(event -> fireEvent(new DeleteEvent(this, binder.getBean()))); + close.addClickListener(event -> fireEvent(new CloseEvent(this))); + + binder.addStatusChangeListener(e -> save.setEnabled(binder.isValid())); + return new HorizontalLayout(save, delete, close); + } + + private void validateAndSave() { + if (binder.isValid()) { + fireEvent(new SaveEvent(this, binder.getBean())); + } + } + + public void setVendor(Vendor vendor) { + binder.setBean(vendor); + } + + public void setCardSets(List sets) { + log.info("Setting card sets: {}", sets); + this.cardSets.setItems(sets); + } + + public abstract static class VendorFormEvent extends ComponentEvent { + private Vendor vendor; + + protected VendorFormEvent(VendorForm source, Vendor vendor) { + super(source, false); + this.vendor = vendor; + } + + public Vendor getVendor() { + return vendor; + } + } + + public static class SaveEvent extends VendorFormEvent { + SaveEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class DeleteEvent extends VendorFormEvent { + DeleteEvent(VendorForm source, Vendor vendor) { + super(source, vendor); + } + } + + public static class CloseEvent extends VendorFormEvent { + CloseEvent(VendorForm source) { + super(source, null); + } + } + + public void addDeleteListener(ComponentEventListener listener) { + addListener(DeleteEvent.class, listener); + } + + public void addSaveListener(ComponentEventListener listener) { + addListener(SaveEvent.class, listener); + } + + public void addCloseListener(ComponentEventListener listener) { + addListener(CloseEvent.class, listener); + } +} diff --git a/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java new file mode 100644 index 0000000..08b5d32 --- /dev/null +++ b/kontor-spring/src/main/java/de/thpeetz/kontor/tysc/views/VendorView.java @@ -0,0 +1,130 @@ +package de.thpeetz.kontor.tysc.views; + +import org.springframework.context.annotation.Scope; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.grid.Grid; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.TextField; +import com.vaadin.flow.data.value.ValueChangeMode; +import com.vaadin.flow.router.PageTitle; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.spring.annotation.SpringComponent; + +import de.thpeetz.kontor.common.views.MainLayout; +import de.thpeetz.kontor.tysc.TyscConstants; +import de.thpeetz.kontor.tysc.data.Vendor; +import de.thpeetz.kontor.tysc.services.CardService; +import jakarta.annotation.security.PermitAll; + +@SpringComponent +@Scope("prototype") +@PermitAll +@Route(value = TyscConstants.VENDOR_ROUTE, layout = MainLayout.class) +@PageTitle("Vendor | Tysc | Kontor") +public class VendorView extends VerticalLayout { + + Grid grid = new Grid<>(Vendor.class); + TextField filterText = new TextField(); + VendorForm form; + CardService service; + + public VendorView(CardService service) { + this.service = service; + addClassName("vendor-view"); + setSizeFull(); + configureGrid(); + configureForm(); + + add(getToolbar(), getContent()); + updateList(); + } + + public Grid getGrid() { + return grid; + } + + private void configureGrid() { + grid.addClassName("vendor-grid"); + grid.setSizeFull(); + grid.setColumns("name"); + grid.getColumns().forEach(col -> col.setAutoWidth(true)); + grid.asSingleSelect().addValueChangeListener(event -> editVendor(event.getValue())); + } + + public VendorForm getForm() { + return form; + } + + private void configureForm() { + form = new VendorForm(); + form.setWidth("25em"); + form.setVisible(false); + form.addSaveListener(this::saveVendor); + form.addDeleteListener(this::deleteVendor); + form.addCloseListener(e -> closeEditor()); + } + + private void saveVendor(VendorForm.SaveEvent event) { + service.saveVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private void deleteVendor(VendorForm.DeleteEvent event) { + service.deleteVendor(event.getVendor()); + updateList(); + closeEditor(); + } + + private Component getContent() { + HorizontalLayout content = new HorizontalLayout(grid, form); + content.setFlexGrow(2, grid); + content.setFlexGrow(1, form); + content.addClassName("content"); + content.setSizeFull(); + return content; + } + + private HorizontalLayout getToolbar() { + filterText.setPlaceholder("Filter by name..."); + filterText.setClearButtonVisible(true); + filterText.setValueChangeMode(ValueChangeMode.LAZY); + filterText.addValueChangeListener(e -> updateList()); + + Button addVendorButton = new Button("Add vendor"); + addVendorButton.addClickListener(click -> addVendor()); + + HorizontalLayout toolbar = new HorizontalLayout(filterText, addVendorButton); + toolbar.addClassName("toolbar"); + return toolbar; + } + + public void editVendor(Vendor vendor) { + if (vendor == null) { + closeEditor(); + } else { + form.setVendor(vendor); + form.setCardSets(vendor.getCardSets()); + form.setVisible(true); + addClassName("editing"); + } + } + + private void closeEditor() { + form.setVendor(null); + form.setVisible(false); + removeClassName("editing"); + } + + private void addVendor() { + grid.asSingleSelect().clear(); + editVendor(new Vendor()); + } + + public void updateList() { + grid.setItems(service.findAllVendors(filterText.getValue())); + } +} diff --git a/kontor-spring/src/main/resources/META-INF/resources/images/offline.png b/kontor-spring/src/main/resources/META-INF/resources/images/offline.png new file mode 100644 index 0000000000000000000000000000000000000000..723e24ce7525be86a728d07a8e98f6e1753b745b GIT binary patch literal 9507 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 literal 0 HcmV?d00001 diff --git a/kontor-spring/src/main/resources/META-INF/resources/offline.html b/kontor-spring/src/main/resources/META-INF/resources/offline.html new file mode 100644 index 0000000..791d1e8 --- /dev/null +++ b/kontor-spring/src/main/resources/META-INF/resources/offline.html @@ -0,0 +1,38 @@ + + + + + + + Offline | Vaadin CRM + + + + +
+ VaadinCRM is offline +

Oh deer, you're offline

+

Your internet connection is offline. Get back online to continue using Vaadin CRM.

+
+ + + diff --git a/kontor-spring/src/main/resources/application.yml b/kontor-spring/src/main/resources/application.yml new file mode 100644 index 0000000..c884eb5 --- /dev/null +++ b/kontor-spring/src/main/resources/application.yml @@ -0,0 +1,87 @@ +app: + name: 'Kontor' + shortName: 'Kontor' + description: 'Kontor is a Spring Boot application' +spring: + profiles: + active: local,dev,test,prod + jpa: + defer-datasource-initialization: true + #hibernate.ddl-auto=create-drop + hibernate: + ddl-auto: update + #ddl-auto: create-drop + show-sql: false + sql: + init: + mode: never + mustache: + check-template-location: false + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: always +logging: + level: + org: + atmosphere: INFO + hibernate: INFO + springframework: + web: INFO + guru: + springframework: + controllers: DEBUG +jwt: + auth: + secret: 'J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc=' +mail: + protocol: 'imap' + host: 'corky.svpdata.eu' + port: 143 + userName: 'thomas.peetz@thpeetz.de' + password: 'fS9f4JYDIO7A' + starttls: true +--- +spring: + config: + activate: + on-profile: prod + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://mariadb:3306/kontor + username: 'kontor' + password: 'kontor' +server: + port: 8000 +--- +spring: + config: + activate: + on-profile: local, dev, test + devtools: + add-properties: false + datasource: + driverClassName: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3306/kontor + username: 'kontor' + password: 'kontor' + #driverClassName: org.hsqldb.jdbc.JDBCDriver + #url: jdbc:hsqldb:file:kontorHSQLDB + #username: 'sa' + #password: 'sa' + #driverClassName: org.sqlite.JDBC + #url: "jdbc:sqlite:file:./kontorDb?cache=shared" + #username=sa + #password=sa + #jpa + #database-platform: org.hibernate.community.dialect.SQLiteDialect +server: + port: 8085 diff --git a/kontor-spring/src/main/resources/banner.txt b/kontor-spring/src/main/resources/banner.txt new file mode 100644 index 0000000..b21c682 --- /dev/null +++ b/kontor-spring/src/main/resources/banner.txt @@ -0,0 +1,8 @@ +,--. ,--. ,--. +| .' / ,---. ,--,--, ,-' '-. ,---. ,--.--. +| . ' | .-. || \'-. .-'| .-. || .--' +| |\ \' '-' '| || | | | ' '-' '| | +`--' '--' `---' `--''--' `--' `---' `--' + +${application.title} ${application.version} +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/kontor-spring/src/main/resources/logback-spring.xml b/kontor-spring/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..b085f8c --- /dev/null +++ b/kontor-spring/src/main/resources/logback-spring.xml @@ -0,0 +1,42 @@ + + + + + + + + + %white(%d{ISO8601}) %highlight(%-5level) [%green(%t)] %yellow(%C{1}) : %msg%n%throwable + + + + + + ${LOGS}/spring-boot-logger.log + + %d %p %C{1} [%t] %m%n + + + + + ${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log + + + 10MB + + + + + + + + + + + + diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/ApplicationTests.java b/kontor-spring/src/test/java/de/thpeetz/kontor/ApplicationTests.java new file mode 100644 index 0000000..abf5c95 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/ApplicationTests.java @@ -0,0 +1,13 @@ +package de.thpeetz.kontor; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApplicationTests { + + @Test + void contextLoads() { + + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java new file mode 100644 index 0000000..bb9d04d --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/TestConstants.java @@ -0,0 +1,19 @@ +package de.thpeetz.kontor.bookshelf; + +public class TestConstants { + + public static final Integer ARTICLEAUTHOR_COUNT = 0; + public static final Integer ARTICLE_COUNT = 0; + public static final Integer AUTHOR_COUNT = 1; + public static final Integer BOOKAUTHOR_COUNT = 0; + public static final Integer BOOK_COUNT = 0; + public static final Integer PUBLISHER_COUNT = 0; + public static final String ARTICLE_TITLE = "Title"; + public static final String AUTHOR_FIRSTNAME = "Firstname"; + public static final String AUTHOR_LASTNAME = "Lastname"; + public static final String PUBLISHER_NIKOL = "Nikol Verlags GmbH"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String PUBLISHER_SEARCH = "kol"; + public static final String BOOK_TITLE = "Book Title"; + public static final String BOOK_ISBN = "978-3-123-467-890"; +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java new file mode 100644 index 0000000..b54b976 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorRepositoryTest.java @@ -0,0 +1,74 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class ArticleAuthorRepositoryTest { + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void saveArticleAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT+1, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByArticle() { + Article article = articleRepository.findByTitle(TestConstants.ARTICLE_TITLE).get(0); + assertEquals(1, articleAuthorRepository.findByArticle(article).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + Author author = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertEquals(1, articleAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteArticleAuthor() { + assertEquals(1, articleAuthorRepository.findAll().size()); + ArticleAuthor articleAuthor = articleAuthorRepository.findAll().get(0); + Author author = articleAuthor.getAuthor(); + author.getArticleAuthors().remove(articleAuthor); + author = authorRepository.save(author); + Article article = articleAuthor.getArticle(); + article.getAuthors().remove(articleAuthor); + article = articleRepository.save(article); + articleAuthorRepository.delete(articleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + articleRepository.delete(article); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java new file mode 100644 index 0000000..bd14fd3 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleAuthorTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class ArticleAuthorTest { + + @Autowired + ArticleRepository articleRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + ArticleAuthorRepository articleAuthorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingEmptyValues() { + ArticleAuthor articleAuthor = new ArticleAuthor(); + assertThrows(TransactionSystemException.class, () -> { + articleAuthorRepository.save(articleAuthor); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + ArticleAuthor articleAuthor = new ArticleAuthor(); + articleAuthor.setArticle(article); + articleAuthor.setAuthor(author); + ArticleAuthor savedArticleAuthor = articleAuthorRepository.save(articleAuthor); + ArticleAuthor articleAuthor1 = new ArticleAuthor(); + articleAuthor1.setArticle(article); + articleAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + articleAuthorRepository.save(articleAuthor1); + }); + articleAuthorRepository.delete(savedArticleAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, articleAuthorRepository.findAll().size()); + savedAuthor.getArticleAuthors().remove(savedArticleAuthor); + authorRepository.save(savedAuthor); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + savedArticle.getAuthors().remove(savedArticleAuthor); + articleRepository.save(savedArticle); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java new file mode 100644 index 0000000..7d74119 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ArticleRepositoryTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } + + @Test + @Order(2) + void saveArticle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + articleRepository.save(article); + assertEquals(TestConstants.ARTICLE_COUNT+1, articleRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE.substring(2,5)); + assertEquals(1, articles.size()); + assertEquals(0, articleRepository.search(TestConstants.BOOK_TITLE).size()); + } + + @Test + @Order(4) + void findByTitle() { + List
articles = articleRepository.search(TestConstants.ARTICLE_TITLE); + assertEquals(1, articles.size()); + } + + @Test + @Order(5) + void deleteArticle() { + List
articles = articleRepository.findAll(); + assertEquals(1, articles.size()); + assertEquals(0, articles.get(0).getAuthors().size()); + articleRepository.delete(articles.get(0)); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java new file mode 100644 index 0000000..2179dfe --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/ArticleTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class ArticleTest { + + @Autowired + ArticleRepository articleRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, articleRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Article article = new Article(); + assertThrows(TransactionSystemException.class, () -> { + articleRepository.save(article); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalTitle() { + Article article = new Article(); + article.setTitle(TestConstants.ARTICLE_TITLE); + Article savedArticle = articleRepository.save(article); + Article article1 = new Article(); + article1.setTitle(article.getTitle()); + assertThrows(DataIntegrityViolationException.class, ()-> { + articleRepository.save(article1); + }); + articleRepository.delete(savedArticle); + assertEquals(TestConstants.ARTICLE_COUNT, articleRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java new file mode 100644 index 0000000..f984d11 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorRepositoryTest.java @@ -0,0 +1,69 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +@Disabled +class AuthorRepositoryTest { + + @Autowired + AuthorRepository authorRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } + + @Test + @Order(2) + void saveAutor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + } + + @Test + @Order(3) + void findAuthor() { + Author existingAuthor = authorRepository.findAll().get(0); + assertNotNull(existingAuthor); + Author found = authorRepository.findByFirstNameAndLastName(existingAuthor.getFirstName(), existingAuthor.getLastName()); + assertEquals(existingAuthor, found); + } + + @Test + @Order(4) + void searchAuthor() { + List authors = authorRepository.search("glas"); + assertEquals(1, authors.size()); + assertEquals("Douglas", authors.get(0).getFirstName()); + List authors2 = authorRepository.search("dams"); + assertEquals(1, authors2.size()); + assertEquals("Adams", authors2.get(0).getLastName()); + } + + @Test + @Order(5) + void deleteAuthor() { + Author existingAuthor = authorRepository.findByFirstNameAndLastName(TestConstants.AUTHOR_FIRSTNAME, TestConstants.AUTHOR_LASTNAME); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java new file mode 100644 index 0000000..149c263 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/AuthorTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class AuthorTest { + + @Autowired + private AuthorRepository authorRepository; + + @Test + void throwExceptionWhenPlayerSavedWithEmptyName() { + Author author = new Author(); + assertThrows(TransactionSystemException.class, () -> { + authorRepository.save(author); + }); + } + + @Test + void throwExceptionWhenPlayerSavedWithExistingName() { + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + Author existingAuthor = new Author(); + existingAuthor.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + existingAuthor.setLastName(TestConstants.AUTHOR_LASTNAME); + existingAuthor = authorRepository.save(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + Author author = new Author(); + author.setFirstName(existingAuthor.getFirstName()); + author.setLastName(existingAuthor.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + authorRepository.save(author); + }); + assertNotNull(existingAuthor); + authorRepository.delete(existingAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java new file mode 100644 index 0000000..4df5fd9 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorRepositoryTest.java @@ -0,0 +1,94 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookAuthorRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void saveBookAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, authorRepository.findAll().size()); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT+1, bookshelfPublisherRepository.findAll().size()); + Book book = new Book(); + book.setTitle(TestConstants.ARTICLE_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(savedBook); + bookAuthor.setAuthor(savedAuthor); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT+1, bookAuthorRepository.findAll().size()); + } + + @Test + @Order(2) + void testFindByBook() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Book book = bookAuthors.get(0).getBook(); + assertEquals(1, bookAuthorRepository.findByBook(book).size()); + } + + @Test + @Order(3) + void testFindByAuthor() { + List bookAuthors = bookAuthorRepository.findAll(); + assertEquals(1, bookAuthors.size()); + Author author = bookAuthors.get(0).getAuthor(); + assertEquals(1, bookAuthorRepository.findByAuthor(author).size()); + } + + @Test + @Order(4) + void deleteBookAuthor() { + BookAuthor bookAuthor = bookAuthorRepository.findAll().get(0); + Author author = bookAuthor.getAuthor(); + author.getBookAuthors().remove(bookAuthor); + author = authorRepository.save(author); + Book book = bookAuthor.getBook(); + book.getAuthors().remove(bookAuthor); + BookshelfPublisher publisher = book.getPublisher(); + publisher.getBooks().remove(book); + bookshelfPublisherRepository.save(publisher); + book = bookRepository.save(book); + bookshelfPublisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(bookAuthor); + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + authorRepository.delete(author); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java new file mode 100644 index 0000000..dc79341 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookAuthorTest.java @@ -0,0 +1,82 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Disabled +class BookAuthorTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + AuthorRepository authorRepository; + + @Autowired + BookAuthorRepository bookAuthorRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOKAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyValues() { + BookAuthor bookAuthor = new BookAuthor(); + assertThrows(TransactionSystemException.class, () -> { + bookAuthorRepository.save(bookAuthor); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalEntry() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + Author savedAuthor = authorRepository.save(author); + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + Book savedBook = bookRepository.save(book); + BookAuthor bookAuthor = new BookAuthor(); + bookAuthor.setBook(book); + bookAuthor.setAuthor(author); + BookAuthor savedBookAuthor = bookAuthorRepository.save(bookAuthor); + BookAuthor bookAuthor1 = new BookAuthor(); + bookAuthor1.setBook(book); + bookAuthor1.setAuthor(author); + assertThrows(DataIntegrityViolationException.class, () -> { + bookAuthorRepository.save(bookAuthor1); + }); + savedAuthor.getBookAuthors().remove(savedBookAuthor); + authorRepository.save(savedAuthor); + savedBook.getAuthors().remove(savedBookAuthor); + bookRepository.save(savedBook); + savedPublisher.getBooks().remove(savedBook); + bookshelfPublisherRepository.save(savedPublisher); + authorRepository.delete(savedAuthor); + assertEquals(TestConstants.AUTHOR_COUNT, authorRepository.findAll().size()); + bookRepository.delete(savedBook); + assertEquals(TestConstants.ARTICLE_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + bookAuthorRepository.delete(savedBookAuthor); + assertEquals(TestConstants.ARTICLEAUTHOR_COUNT, bookAuthorRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java new file mode 100644 index 0000000..969d465 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookRepositoryTest.java @@ -0,0 +1,87 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@Disabled +class BookRepositoryTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + @Order(1) + void checkInitialLoad(){ + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + @Order(2) + void saveBook() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(publisher); + bookRepository.save(book); + assertEquals(TestConstants.BOOK_COUNT+1, bookRepository.findAll().size()); + } + + @Test + @Order(3) + void search() { + List books = bookRepository.search(TestConstants.BOOK_TITLE.substring(2,5)); + assertEquals(1, books.size()); + assertEquals(1, bookRepository.search(TestConstants.BOOK_ISBN.substring(3,7)).size()); + assertEquals(0, bookRepository.search("Article").size()); + } + + @Test + @Order(4) + void findByTitle() { + List books = bookRepository.findByTitle(TestConstants.BOOK_TITLE); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(5) + void findByTitleIgnoreCase() { + List books = bookRepository.findByTitleIgnoreCase(TestConstants.BOOK_TITLE.toLowerCase()); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_ISBN, books.get(0).getIsbn()); + } + + @Test + @Order(6) + void findByIsbn() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + assertEquals(1, books.size()); + assertEquals(TestConstants.BOOK_TITLE, books.get(0).getTitle()); + } + + @Test + @Order(7) + void deleteBook() { + List books = bookRepository.findByIsbn(TestConstants.BOOK_ISBN); + Book book = books.get(0); + BookshelfPublisher publisher = book.getPublisher(); + bookshelfPublisherRepository.delete(publisher); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} \ No newline at end of file diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java new file mode 100644 index 0000000..0ac3324 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookTest.java @@ -0,0 +1,56 @@ +package de.thpeetz.kontor.bookshelf.data; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.comics.data.Publisher; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class BookTest { + + @Autowired + BookRepository bookRepository; + + @Autowired + BookshelfPublisherRepository bookshelfPublisherRepository; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingEmptyFields() { + Book book = new Book(); + assertThrows(TransactionSystemException.class, () -> { + bookRepository.save(book); + }); + } + + @Test + void exceptionThrownWhenSavingIdenticalISBN() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + BookshelfPublisher savedPublisher = bookshelfPublisherRepository.save(publisher); + Book book = new Book(); + book.setTitle(TestConstants.BOOK_TITLE); + book.setIsbn(TestConstants.BOOK_ISBN); + book.setPublisher(savedPublisher); + Book savedBook = bookRepository.save(book); + Book book1 = new Book(); + book1.setTitle(book.getTitle()); + book1.setIsbn(book.getIsbn()); + assertThrows(TransactionSystemException.class, ()-> { + bookRepository.save(book1); + }); + bookRepository.delete(book); + assertEquals(TestConstants.BOOK_COUNT, bookRepository.findAll().size()); + bookshelfPublisherRepository.delete(savedPublisher); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfPublisherRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java new file mode 100644 index 0000000..98b60fa --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherRepositoryTest.java @@ -0,0 +1,72 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfPublisherRepositoryTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + log.info("Liste der Publisher: {}", publisherRepository.findAll()); + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + BookshelfPublisher notFound = publisherRepository.findByName(TestConstants.PUBLISHER_SEARCH); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search(TestConstants.PUBLISHER_NAME.substring(2, 6)); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + BookshelfPublisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java new file mode 100644 index 0000000..7a73ae1 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/data/BookshelfPublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.bookshelf.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.bookshelf.TestConstants; + +@SpringBootTest +class BookshelfPublisherTest { + + @Autowired + private BookshelfPublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + BookshelfPublisher publisher = new BookshelfPublisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + BookshelfPublisher publisher1 = new BookshelfPublisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + BookshelfPublisher publisher2 = new BookshelfPublisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java new file mode 100644 index 0000000..b78a899 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/bookshelf/services/BookshelfServiceTest.java @@ -0,0 +1,102 @@ +package de.thpeetz.kontor.bookshelf.services; + +import de.thpeetz.kontor.bookshelf.TestConstants; +import de.thpeetz.kontor.bookshelf.data.Author; +import de.thpeetz.kontor.bookshelf.data.Book; +import de.thpeetz.kontor.bookshelf.data.BookshelfPublisher; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class BookshelfServiceTest { + + @Autowired + private BookshelfService bookshelfService; + + @Test + @Order(1) + void testFindAllPublishers() { + List publishers = bookshelfService.findAllPublishers(null); + assertEquals(TestConstants.PUBLISHER_COUNT, publishers.size()); + } + + @Test + @Order(2) + void testSavePublisher() { + BookshelfPublisher publisher = new BookshelfPublisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + bookshelfService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(3) + void testFindPublisherByName() { + assertEquals(TestConstants.PUBLISHER_COUNT + 1, bookshelfService.findAllPublishers(null).size()); + BookshelfPublisher publisher = bookshelfService.findPublisherByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + } + + @Test + @Order(4) + void testDeletePublisher() { + List publishers = bookshelfService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + bookshelfService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, bookshelfService.findAllPublishers(null).size()); + } + + @Test + @Order(5) + void testFindAllAuthors() { + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(6) + void testSaveAuthor() { + Author author = new Author(); + author.setFirstName(TestConstants.AUTHOR_FIRSTNAME); + author.setLastName(TestConstants.AUTHOR_LASTNAME); + bookshelfService.saveAuthor(author); + assertEquals(TestConstants.AUTHOR_COUNT+1, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(7) + void testDeleteAuthor() { + List authors = bookshelfService.findAllAuthors(TestConstants.AUTHOR_FIRSTNAME); + assertEquals(1, authors.size()); + bookshelfService.deleteAuthor(authors.get(0)); + assertEquals(TestConstants.AUTHOR_COUNT, bookshelfService.findAllAuthors(null).size()); + } + + @Test + @Order(8) + void findAllBooks() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(9) + void saveBook() { + assertEquals(TestConstants.BOOK_COUNT, bookshelfService.findAllBooks(null).size()); + } + + @Test + @Order(10) + void deleteBook() { + List books = bookshelfService.findAllBooks(null); + assertEquals(0, books.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java new file mode 100644 index 0000000..b1e50ce --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/ComicConstantsTest.java @@ -0,0 +1,14 @@ +package de.thpeetz.kontor.comics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ComicConstantsTest { + + @Test + void getArtistConstants() { + assertEquals("Artist", ComicConstants.ARTIST); + assertEquals("comics/artist", ComicConstants.ARTIST_ROUTE); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/TestConstants.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/TestConstants.java new file mode 100644 index 0000000..b77f55d --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/TestConstants.java @@ -0,0 +1,67 @@ +package de.thpeetz.kontor.comics; + +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.Worktype; +import de.thpeetz.kontor.comics.services.ComicService; + +public class TestConstants { + public static final String ARTIST_NAME = "Lastname, Firstname"; + public static final String COMIC_TITLE = "TestComic"; + public static final String WORKTYPE_INKER = "Inker"; + public static final String ISSUE_ISSUENUMBER = "Issuenumber"; + public static final String PUBLISHER_NAME = "Publisher"; + public static final String SOJOURN_TPB_COMIC_TITLE = "Sojourn"; + public static final String SOJOURN_TPB_NAME = "The Dragons Tale"; + public static final String STORYARC_NAME = "StoryArc"; + public static final String TRADEPAPERBACK_NAME = "TradePaperback"; + public static final String VOLUME_NAME = "Volume"; + public static final String WORKTYPE_NAME = "Worktype"; + public static final int ARTIST_COUNT = 5; + public static final int BATTLE_POPE_ISSUE_COUNT = 12; + public static final int COMIC_COUNT = 169; + public static final int COMICWORK_COUNT = 18; + public static final int ISSUE_COUNT = 750; + public static final int MARVEL_COMIC_COUNT = 50; + public static final int PUBLISHER_COUNT = 18; + public static final int SOJOURN_TPB_COUNT = 4; + public static final int SOJOURN_TPB_START = 7; + public static final int SOJOURN_TPB_END = 12; + public static final int STORYARC_COUNT = 3; + public static final int TRADEPAPERBACK_COUNT = 40; + public static final int VOLUME_COUNT = 0; + public static final int WORKTYPE_COUNT = 3; + + public static Comic getComicWithStoryArcs(ComicService service) { + return service.findComicByTitle("Emma Frost"); + } + + public static Comic getComicWithTradePaperbacks(ComicService service) { + return service.findComicByTitle(SOJOURN_TPB_COMIC_TITLE); + } + + public static Comic getComicWithIssues(ComicService service) { + return service.findComicByTitle("Battle Pope"); + } + + public static Comic getComicWithoutReferences(ComicService service) { + return service.findComicByTitle("Gen13"); + } + + public static Artist getArtistWithoutReferences(ComicService service) { + return service.findArtistByName("Marz, Ron"); + } + + public static Worktype getWorktypeWithoutReferences(ComicService service) { + return service.findWorktypeByName(WORKTYPE_INKER); + } + + public static Publisher getPublisherForComicTests(ComicService service) { + return service.findPublisherByName("Marvel"); + } + + public static Publisher getAnotherPublisherForComicTests(ComicService service) { + return service.findPublisherByName("DC"); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java new file mode 100644 index 0000000..96d53f8 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistRepositoryTest.java @@ -0,0 +1,73 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ArtistRepositoryTest { + + private Artist stanLee; + private static final String ARTISTNAME = "Lee, Stan"; + + @Autowired + ArtistRepository artistRepository; + + @BeforeEach + void setupData() { + stanLee = new Artist(); + stanLee.setName(ARTISTNAME); + } + + @Test + @Order(1) + void checkInitialLoad() { + int count = artistRepository.findAll().size(); + assertEquals(5, count); + } + + @Test + @Order(2) + void saveArtist() { + int count = artistRepository.findAll().size(); + artistRepository.save(stanLee); + assertEquals(count+1, artistRepository.findAll().size()); + } + + @Test + @Order(3) + void findArtist() { + List artists = artistRepository.findByNameIgnoreCase("lEE, sTAN"); + assertTrue(artists.size() > 0); + assertEquals(artists.get(0).getName(), stanLee.getName()); + } + + @Test + @Order(4) + void searchArtist() { + List artists = artistRepository.search("Lee"); + assertEquals(1, artists.size()); + assertEquals(ARTISTNAME, artists.get(0).getName()); + List artists2 = artistRepository.search("Stan"); + assertEquals(1, artists2.size()); + assertEquals(ARTISTNAME, artists2.get(0).getName()); + } + + @Test + @Order(5) + void deleteArtist() { + int count = artistRepository.findAll().size(); + Artist artist = artistRepository.findByName(ARTISTNAME); + artistRepository.delete(artist); + assertEquals(count-1, artistRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java new file mode 100644 index 0000000..855795e --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ArtistTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class ArtistTest { + + @Autowired + private ArtistRepository artistRepository; + + @Test + void checkInitialDataLoad() { + List artists = artistRepository.findAll(); + assertEquals(TestConstants.ARTIST_COUNT, artists.size()); + } + + @Test + void throwExceptionWhenArtistSavedWithEmptyName() { + Artist artist1 = new Artist(); + assertThrows(TransactionSystemException.class, () -> { + artistRepository.save(artist1); + }); + } + + @Test + void saveArtistWithIdenticalName() { + String artistName = "Lastname, Firstname"; + Artist artist1 = new Artist(); + artist1.setName(artistName); + artistRepository.save(artist1); + Artist artist2 = new Artist(); + artist2.setName(artistName); + assertThrows(DataIntegrityViolationException.class, () -> { + artistRepository.save(artist2); + }); + artistRepository.delete(artist1); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java new file mode 100644 index 0000000..70c0d54 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicRepositoryTest.java @@ -0,0 +1,53 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ComicRepositoryTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void testFindByTitle() { + List comics = comicRepository.findByTitle("Emma Frost"); + assertEquals(1, comics.size()); + assertEquals("Emma Frost", comics.get(0).getTitle()); + Publisher publisher = comics.get(0).getPublisher(); + assertEquals("Marvel", publisher.getName()); + } + + @Test + void testFindByTitleIgnoreCase() { + List comics = comicRepository.findByTitleIgnoreCase("x-men"); + assertEquals(1, comics.size()); + assertEquals("X-Men", comics.get(0).getTitle()); + } + @Test + void testFindByTitleAndPublisher() { + Publisher publisher = publisherRepository.findByName("Marvel"); + assertNotNull(publisher); + Comic found = comicRepository.findByTitleAndPublisher("Emma Frost", publisher); + assertNotNull(found); + assertEquals("Emma Frost", found.getTitle()); + assertEquals("Marvel", found.getPublisher().getName()); + + } + + @Test + void testSearch() { + List comics = comicRepository.search("X-men"); + assertEquals(11, comics.size()); + assertTrue(comics.stream().map(comic -> comic.getTitle()).collect(Collectors.toList()).contains("Astonishing X-Men")); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java new file mode 100644 index 0000000..5560346 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicTest.java @@ -0,0 +1,77 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicTest { + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialDataLoad() { + List comics = comicRepository.findAll(); + assertEquals(TestConstants.COMIC_COUNT, comics.size()); + } + + @Test + @Order(2) + void exceptionThrownWhenSavingComicWithoutPublisher() { + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(3) + void exceptionThrownWhenSavingComicWithEmptyTitle() { + Comic comic = new Comic(); + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + comic.setPublisher(publisher); + assertNull(comic.getTitle()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + comic.setTitle(""); + assertTrue(comic.getTitle().isEmpty()); + assertThrows(TransactionSystemException.class, () -> { + comicRepository.save(comic); + }); + } + + @Test + @Order(4) + void exceptionThrownWhenSavingComicWithSameNameFromDifferentPublishers() { + Publisher publisher = TestConstants.getPublisherForComicTests(comicService); + Comic comic = new Comic(); + comic.setTitle(TestConstants.SOJOURN_TPB_COMIC_TITLE); + comic.setPublisher(publisher); + assertThrows(DataIntegrityViolationException.class, () -> { + comicRepository.save(comic); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java new file mode 100644 index 0000000..0902b84 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkRepositoryTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicWorkRepositoryTest { + + @Autowired + private ComicWorkRepository comicWorkRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private WorktypeRepository worktypeRepository; + + @Autowired + private ArtistRepository artistRepository; + + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void checkInitialLoad() { + int count = comicWorkRepository.findAll().size(); + assertEquals(18, count); + } + + @Test + @Order(2) + void saveComicWork() { + int count = comicWorkRepository.findAll().size(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = new ComicWork(); + comicWork.setArtist(artist); + comicWork.setComic(comic); + comicWork.setWorkType(worktype); + comicWorkRepository.save(comicWork); + assertEquals(count + 1, comicWorkRepository.findAll().size()); + } + + @Test + @Order(3) + void findByComicAndArtistAndComicWork() { + assertEquals(19, comicWorkRepository.count()); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + assertEquals(comicWork.getArtist().getName(), artist.getName()); + assertEquals(comicWork.getComic().getTitle(), comic.getTitle()); + assertEquals(comicWork.getWorkType().getName(), worktype.getName()); + ComicWork notFound = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, null); + assertNull(notFound); + } + + @Test + @Order(4) + void deleteComicWork() { + long count = comicWorkRepository.count(); + Artist artist = artistRepository.findByName("Turner, Michael"); + Worktype worktype = worktypeRepository.findByName("Writer"); + Comic comic = comicRepository.findByTitle("Emma Frost").get(0); + ComicWork comicWork = comicWorkRepository.findbyComicAndArtistAndWorktype(comic, artist, worktype); + assertNotNull(comicWork); + comicService.deleteComicWork(comicWork); + assertEquals(count - 1, comicWorkRepository.count()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java new file mode 100644 index 0000000..ac2eac2 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/ComicWorkTest.java @@ -0,0 +1,24 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +public class ComicWorkTest { + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List comicWorks = comicService.findAllComicWorks(); + assertEquals(18, comicWorks.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java new file mode 100644 index 0000000..8d3be93 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueRepositoryTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = issueRepository.findByComic(comic); + assertEquals(TestConstants.BATTLE_POPE_ISSUE_COUNT, issues.size()); + } + + @Test + void testFindByComicAndIssueNumber() { + Comic comic = TestConstants.getComicWithIssues(comicService); + Issue issue = issueRepository.findByComicAndIssueNumber(comic, "12"); + assertNotNull(issue); + assertFalse(issue.getIsRead()); + } + + @Test + void testSearch() { + List issues = issueRepository.search("2"); + assertEquals(187, issues.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java new file mode 100644 index 0000000..1760fd1 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/IssueTest.java @@ -0,0 +1,62 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class IssueTest { + + @Autowired + private IssueRepository issueRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + List issues = comicService.findAllIssues(); + assertEquals(TestConstants.ISSUE_COUNT, issues.size()); + } + + @Test + void exceptionThrownWhenSavingIssueWithEmptyNumber() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + Issue issue = new Issue(); + issue.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + Issue issue = new Issue(); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + assertThrows(TransactionSystemException.class, () -> { + issueRepository.save(issue); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + Issue savedInstance = issueRepository.save(issue); + assertNotNull(savedInstance); + comicService.deleteIssue(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java new file mode 100644 index 0000000..50175b4 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherRepositoryTest.java @@ -0,0 +1,68 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PublisherRepositoryTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + @Order(2) + void savePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, publisherRepository.findAll().size()); + } + + @Test + @Order(3) + void findPublisherByName() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + assertEquals(TestConstants.PUBLISHER_NAME, publisher.getName()); + Publisher notFound = publisherRepository.findByName("Cow"); + assertNull(notFound); + List publishers = publisherRepository + .findByNameIgnoreCase(TestConstants.PUBLISHER_NAME.toLowerCase()); + assertEquals(1, publishers.size()); + assertEquals(TestConstants.PUBLISHER_NAME, publishers.get(0).getName()); + } + + @Test + @Order(4) + void searchPublisher() { + List publishers = publisherRepository.search("Cow"); + assertEquals(1, publishers.size()); + assertEquals("Top Cow Productions", publishers.get(0).getName()); + } + + @Test + @Order(5) + void deletePublisher() { + Publisher publisher = publisherRepository.findByName(TestConstants.PUBLISHER_NAME); + assertNotNull(publisher); + publisherRepository.delete(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java new file mode 100644 index 0000000..264a1d0 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/PublisherTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class PublisherTest { + + @Autowired + private PublisherRepository publisherRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } + + @Test + void throwExceptionWhenPublisherSavedWithEmptyName() { + Publisher publisher = new Publisher(); + assertThrows(TransactionSystemException.class, () -> { + publisherRepository.save(publisher); + }); + } + + @Test + void savePublisherWithIdenticalName() { + Publisher publisher1 = new Publisher(); + publisher1.setName(TestConstants.PUBLISHER_NAME); + publisherRepository.save(publisher1); + Publisher publisher2 = new Publisher(); + publisher2.setName(TestConstants.PUBLISHER_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + publisherRepository.save(publisher2); + }); + publisherRepository.delete(publisher1); + assertEquals(TestConstants.PUBLISHER_COUNT, publisherRepository.findAll().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java new file mode 100644 index 0000000..52bad7e --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcRepositoryTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcRepositoryTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List storyArcs = storyArcRepository.search("Learn"); + assertNotNull(storyArcs); + assertEquals(1, storyArcs.size()); + } + + @Test + void findByComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + List storyArcs = storyArcRepository.findByComic(comic); + assertNotNull(storyArcs); + assertEquals(3, storyArcs.size()); + } + + @Test + void findByNameAndComic() { + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + StoryArc storyArc = storyArcRepository.findByNameAndComic("Higher Learning", comic); + assertNotNull(storyArc); + assertEquals("Emma Frost", storyArc.getComic().getTitle()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java new file mode 100644 index 0000000..07134ca --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/StoryArcTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class StoryArcTest { + + @Autowired + private StoryArcRepository storyArcRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialLoad() { + assertEquals(TestConstants.STORYARC_COUNT, storyArcRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + StoryArc storyArc = new StoryArc(); + storyArc.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void exceptionThrownWhenSavingStoryArcWithoutComic() { + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + assertThrows(TransactionSystemException.class, () -> { + storyArcRepository.save(storyArc); + }); + } + + @Test + void saveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getStoryArcs().size()); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + StoryArc savedInstance = storyArcRepository.save(storyArc); + assertNotNull(savedInstance); + comicService.deleteStoryArc(savedInstance); + assertEquals(0, comic.getStoryArcs().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java new file mode 100644 index 0000000..2c66c12 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackRepositoryTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackRepositoryTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void testSearch() { + List tradePaperbacks = tradePaperbackRepository.search("Dragon"); + assertNotNull(tradePaperbacks); + assertEquals(1, tradePaperbacks.size()); + } + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository.findByComic(comic); + assertNotNull(tradePaperbacks); + assertEquals(TestConstants.SOJOURN_TPB_COUNT, tradePaperbacks.size()); + } + + @Test + void testFindByNameAndComic() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + List tradePaperbacks = tradePaperbackRepository + .findByNameAndComic(TestConstants.SOJOURN_TPB_NAME, comic); + assertNotNull(tradePaperbacks); + assertTrue(tradePaperbacks.size() > 0); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperbacks.get(0).getComic().getTitle()); + } + + @Test + void testFindByFields() { + Comic comic = TestConstants.getComicWithTradePaperbacks(comicService); + TradePaperback tradePaperback = tradePaperbackRepository.findByFields(TestConstants.SOJOURN_TPB_NAME, + comic, TestConstants.SOJOURN_TPB_START, TestConstants.SOJOURN_TPB_END); + assertNotNull(tradePaperback); + assertEquals(TestConstants.SOJOURN_TPB_COMIC_TITLE, tradePaperback.getComic().getTitle()); + assertEquals(TestConstants.SOJOURN_TPB_END, tradePaperback.getIssueEnd()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java new file mode 100644 index 0000000..61df1d6 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/TradePaperbackTest.java @@ -0,0 +1,59 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class TradePaperbackTest { + + @Autowired + private TradePaperbackRepository tradePaperbackRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, tradePaperbackRepository.findAll().size()); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void exceptionThrownWhenSavingTradePaperbackWithoutComic() { + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + assertThrows(TransactionSystemException.class, () -> { + tradePaperbackRepository.save(tradePaperback); + }); + } + + @Test + void saveTradePaperback() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getTradePaperbacks().size()); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + tradePaperback.setComic(comic); + TradePaperback savedInstance = tradePaperbackRepository.save(tradePaperback); + assertNotNull(savedInstance); + comicService.deleteTradePaperBack(savedInstance); + assertEquals(0, comic.getTradePaperbacks().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java new file mode 100644 index 0000000..ce04a11 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeRepositoryTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void testFindByComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByComic(comic); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void testFindByName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + + List found = volumeRepository.findByName(TestConstants.VOLUME_NAME); + assertEquals(1, found.size()); + + comicService.deleteVolume(found.get(0)); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java new file mode 100644 index 0000000..7189965 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/VolumeTest.java @@ -0,0 +1,64 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.services.ComicService; + +@SpringBootTest +class VolumeTest { + + @Autowired + private VolumeRepository volumeRepository; + + @Autowired + private ComicRepository comicRepository; + + @Autowired + private ComicService comicService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } + + @Test + void exceptionThrownWhenSavingVolumeWithEmptyName() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setComic(comic); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void exceptionThrownWhenSavingVolumeWithoutComic() { + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + assertThrows(TransactionSystemException.class, () -> { + volumeRepository.save(volume); + }); + } + + @Test + void saveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + assertEquals(0, comic.getVolumes().size()); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + Volume savedInstance = volumeRepository.save(volume); + assertNotNull(savedInstance); + comicService.deleteVolume(savedInstance); + assertEquals(0, comic.getVolumes().size()); + assertEquals(TestConstants.VOLUME_COUNT, volumeRepository.count()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java new file mode 100644 index 0000000..5dae5e8 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeRepositoryTest.java @@ -0,0 +1,46 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +class WorktypeRepositoryTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName("Writer"); + assertNotNull(found); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + void findWorktypeByNameIgnoreCase() { + List worktypes = worktypeRepository.findByNameIgnoreCase("Writer".toLowerCase()); + assertNotNull(worktypes); + assertEquals(1, worktypes.size()); + assertEquals("Writer", worktypes.get(0).getName()); + } + + @Test + void searchWorktype() { + List worktypes = worktypeRepository.search("er"); + assertEquals(3, worktypes.size()); + assertTrue(worktypes.stream().map(worktype -> worktype.getName()).collect(Collectors.toList()).contains("Writer")); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java new file mode 100644 index 0000000..3145ee9 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/data/WorktypeTest.java @@ -0,0 +1,83 @@ +package de.thpeetz.kontor.comics.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.orm.jpa.JpaSystemException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.comics.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class WorktypeTest { + + @Autowired + private WorktypeRepository worktypeRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List worktypes = worktypeRepository.findAll(); + assertEquals(3, worktypes.size()); + } + + @Test + @Order(2) + void findWorktypeByName() { + Worktype found = worktypeRepository.findByName(TestConstants.WORKTYPE_INKER); + assertNotNull(found); + assertEquals(TestConstants.WORKTYPE_INKER, found.getName()); + Worktype notFound = worktypeRepository.findByName("er"); + assertNull(notFound); + } + + @Test + @Order(3) + void throwExceptionWhenArtistSavedWithEmptyName() { + Worktype worktype1 = new Worktype(); + assertThrows(TransactionSystemException.class, () -> { + worktypeRepository.save(worktype1); + }); + } + + @Test + @Order(4) + void saveWorktypeWithIdenticalName() { + Worktype worktype1 = new Worktype(); + worktype1.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype1); + Worktype worktype2 = new Worktype(); + worktype2.setName(TestConstants.WORKTYPE_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + worktypeRepository.save(worktype2); + }); + worktypeRepository.delete(worktype1); + } + + @Test + @Order(5) + void saveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + worktypeRepository.save(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, worktypeRepository.count()); + } + + @Test + @Order(6) + void deleteWorktype() { + Worktype worktype = worktypeRepository.findByName(TestConstants.WORKTYPE_NAME); + assertNotNull(worktype); + worktypeRepository.delete(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT, worktypeRepository.count()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java new file mode 100644 index 0000000..d449298 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/comics/services/ComicServiceTest.java @@ -0,0 +1,350 @@ +package de.thpeetz.kontor.comics.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.comics.TestConstants; +import de.thpeetz.kontor.comics.data.Artist; +import de.thpeetz.kontor.comics.data.Comic; +import de.thpeetz.kontor.comics.data.ComicWork; +import de.thpeetz.kontor.comics.data.Issue; +import de.thpeetz.kontor.comics.data.Publisher; +import de.thpeetz.kontor.comics.data.StoryArc; +import de.thpeetz.kontor.comics.data.TradePaperback; +import de.thpeetz.kontor.comics.data.Volume; +import de.thpeetz.kontor.comics.data.Worktype; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class ComicServiceTest { + + private static final Logger log = LoggerFactory.getLogger(ComicServiceTest.class); + @Autowired + private ComicService comicService; + + @Test + @Order(1) + void testFindAllArtists() { + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + assertEquals(1, comicService.findAllArtists("Turn").size()); + } + + @Test + @Order(1) + void testFindArtistByName() { + assertNull(comicService.findArtistByName("Lee, Stan")); + Artist artist = comicService.findArtistByName("Turner, Michael"); + assertNotNull(artist); + assertNotNull(artist.getComicWorks()); + assertFalse(artist.getComicWorks().isEmpty()); + } + + @Test + @Order(2) + void testSaveArtist() { + Artist artist = new Artist(); + artist.setName(TestConstants.ARTIST_NAME); + comicService.saveArtist(artist); + assertEquals(TestConstants.ARTIST_COUNT + 1, comicService.findAllArtists(null).size()); + } + + @Test + @Order(3) + void testDeleteArtist() { + List artists = comicService.findAllArtists(TestConstants.ARTIST_NAME); + assertEquals(1, artists.size()); + comicService.deleteArtist(artists.get(0)); + assertEquals(TestConstants.ARTIST_COUNT, comicService.findAllArtists(null).size()); + } + + @Test + @Order(4) + void testFindAllPublishers() { + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + assertEquals(1, comicService.findAllPublishers("Cow").size()); + } + + @Test + @Order(4) + void testFindPublisherByName() { + Publisher publisher = comicService.findPublisherByName("Marvel"); + assertNotNull(publisher); + assertNotNull(publisher.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, publisher.getComics().size()); + assertNull(comicService.findPublisherByName(TestConstants.PUBLISHER_NAME)); + } + + @Test + @Order(5) + void testSavePublisher() { + Publisher publisher = new Publisher(); + publisher.setName(TestConstants.PUBLISHER_NAME); + comicService.savePublisher(publisher); + assertEquals(TestConstants.PUBLISHER_COUNT + 1, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(6) + void testDeletePublisher() { + List publishers = comicService.findAllPublishers(TestConstants.PUBLISHER_NAME); + assertEquals(1, publishers.size()); + comicService.deletePublisher(publishers.get(0)); + assertEquals(TestConstants.PUBLISHER_COUNT, comicService.findAllPublishers(null).size()); + } + + @Test + @Order(7) + void testFindAllWorktypes() { + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + assertEquals(1, comicService.findAllWorktypes("ite").size()); + } + + @Test + @Order(7) + void testFindWorktypeByName() { + Worktype worktype = comicService.findWorktypeByName("Writer"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertFalse(worktype.getComicWorks().isEmpty()); + worktype = comicService.findWorktypeByName("Inker"); + assertNotNull(worktype); + assertNotNull(worktype.getComicWorks()); + assertTrue(worktype.getComicWorks().isEmpty()); + assertNull(comicService.findWorktypeByName(TestConstants.WORKTYPE_NAME)); + } + + @Test + @Order(8) + void testSaveWorktype() { + Worktype worktype = new Worktype(); + worktype.setName(TestConstants.WORKTYPE_NAME); + comicService.saveWorktype(worktype); + assertEquals(TestConstants.WORKTYPE_COUNT + 1, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(9) + void testDeleteWorktype() { + List worktypes = comicService.findAllWorktypes(TestConstants.WORKTYPE_NAME); + assertEquals(1, worktypes.size()); + comicService.deleteWorktype(worktypes.get(0)); + assertEquals(TestConstants.WORKTYPE_COUNT, comicService.findAllWorktypes(null).size()); + } + + @Test + @Order(10) + void testFindComicByTitle() { + assertNull(comicService.findComicByTitle(null)); + assertNotNull(comicService.findComicByTitle("Danger Girl")); + assertNull(comicService.findComicByTitle("x-men")); + } + + @Test + @Order(11) + void testSaveComic() { + Publisher marvel = comicService.findAllPublishers("Marvel").get(0); + Comic comic = new Comic(); + comic.setTitle(TestConstants.COMIC_TITLE); + comic.setPublisher(marvel); + comicService.saveComic(comic); + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + } + + @Test + @Order(12) + void testDeleteComic() { + assertEquals(TestConstants.COMIC_COUNT + 1, comicService.findAllComics(null).size()); + List comics = comicService.findAllComics(TestConstants.COMIC_TITLE); + assertEquals(1, comics.size()); + Comic comic = comicService.findComicByTitle(TestConstants.COMIC_TITLE); + assertNotNull(comic); + comicService.deleteComic(comic); + Publisher marvel = comicService.findPublisherByName("Marvel"); + assertNotNull(marvel.getComics()); + assertEquals(TestConstants.MARVEL_COMIC_COUNT, marvel.getComics().size()); + assertEquals(0, comicService.findAllComics(TestConstants.COMIC_TITLE).size()); + assertEquals(TestConstants.COMIC_COUNT, comicService.findAllComics(null).size()); + } + + @Test + @Order(13) + void testFindAllIssues() { + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(14) + void testFindAllIssuesForComic() { + Comic comic = TestConstants.getComicWithIssues(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(12, issues.size()); + } + + @Test + @Order(15) + void testSaveIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Issue issue = new Issue(); + issue.setComic(comic); + issue.setIssueNumber(TestConstants.ISSUE_ISSUENUMBER); + comicService.saveIssue(issue); + assertEquals(TestConstants.ISSUE_COUNT + 1, comicService.findAllIssues().size()); + comic = TestConstants.getComicWithoutReferences(comicService); + assertNotNull(comic.getIssues()); + assertEquals(1, comic.getIssues().size()); + } + + @Test + @Order(16) + void testDeleteIssue() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List issues = comicService.findAllIssuesForComic(comic); + assertEquals(1, issues.size()); + comicService.deleteIssue(issues.get(0)); + assertEquals(TestConstants.ISSUE_COUNT, comicService.findAllIssues().size()); + } + + @Test + @Order(17) + void testFindAllTradePaperbacks() { + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + assertEquals(7, comicService.findAllTradePaperbacks("of").size()); + } + + @Test + @Order(18) + void testSaveTradePaperBack() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + TradePaperback tradePaperback = new TradePaperback(); + tradePaperback.setComic(comic); + tradePaperback.setName(TestConstants.TRADEPAPERBACK_NAME); + comicService.saveTradePaperBack(tradePaperback); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT + 1, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(19) + void testDeleteTradePaperBack() { + List tradePaperbacks = comicService.findAllTradePaperbacks(TestConstants.TRADEPAPERBACK_NAME); + assertEquals(1, tradePaperbacks.size()); + comicService.deleteTradePaperBack(tradePaperbacks.get(0)); + assertEquals(TestConstants.TRADEPAPERBACK_COUNT, comicService.findAllTradePaperbacks(null).size()); + } + + @Test + @Order(20) + void testFindAllStoryArcs() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(21) + void testFindAllStoryArcsForComic() { + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcsForComic(null).size()); + Comic comic = TestConstants.getComicWithStoryArcs(comicService); + assertEquals(3, comicService.findAllStoryArcsForComic(comic).size()); + } + + @Test + @Order(22) + void testSaveStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + StoryArc storyArc = new StoryArc(); + storyArc.setName(TestConstants.STORYARC_NAME); + storyArc.setComic(comic); + comicService.saveStoryArc(storyArc); + assertEquals(TestConstants.STORYARC_COUNT + 1, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(23) + void testDeleteStoryArc() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List storyArcs = comicService.findAllStoryArcsForComic(comic); + assertEquals(1, storyArcs.size()); + comicService.deleteStoryArc(storyArcs.get(0)); + assertEquals(TestConstants.STORYARC_COUNT, comicService.findAllStoryArcs().size()); + } + + @Test + @Order(24) + void testFindAllVolumes() { + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(25) + void testSaveVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Volume volume = new Volume(); + volume.setName(TestConstants.VOLUME_NAME); + volume.setComic(comic); + comicService.saveVolume(volume); + assertEquals(TestConstants.VOLUME_COUNT + 1, comicService.findAllVolumes().size()); + } + + @Test + @Order(26) + void testFindAllVolumesForComic() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + assertEquals(1, volumes.size()); + } + + @Test + @Order(27) + void testDeleteVolume() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + List volumes = comicService.findAllVolumesForComic(comic); + comicService.deleteVolume(volumes.get(0)); + assertEquals(TestConstants.VOLUME_COUNT, comicService.findAllVolumes().size()); + } + + @Test + @Order(28) + void testFindAllComicWorks() { + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } + + @Test + @Order(29) + void testSaveComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + ComicWork comicWork = new ComicWork(); + comicWork.setComic(comic); + comicWork.setArtist(artist); + comicWork.setWorkType(worktype); + comicService.saveComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT + 1, comicService.findAllComicWorks().size()); + } + + @Test + @Order(30) + void testDeleteComicWork() { + Comic comic = TestConstants.getComicWithoutReferences(comicService); + Artist artist = TestConstants.getArtistWithoutReferences(comicService); + Worktype worktype = TestConstants.getWorktypeWithoutReferences(comicService); + List comicWorks = comic.getComicWorks(); + assertNotNull(comicWorks); + assertNotNull(artist.getComicWorks()); + assertNotNull(worktype.getComicWorks()); + assertEquals(1, comicWorks.size()); + assertEquals(1, artist.getComicWorks().size()); + assertEquals(1, worktype.getComicWorks().size()); + ComicWork comicWork = comicWorks.get(0); + comicService.deleteComicWork(comicWork); + assertEquals(TestConstants.COMICWORK_COUNT, comicService.findAllComicWorks().size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/TestConstants.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/TestConstants.java new file mode 100644 index 0000000..32ee883 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/TestConstants.java @@ -0,0 +1,5 @@ +package de.thpeetz.kontor.media; + +public class TestConstants { + public static final String URL = "https://example.com/link.mp4"; +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java new file mode 100644 index 0000000..2f2adc7 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaArticleTest.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.media.data; + +import de.thpeetz.kontor.media.services.MediaArticleService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaArticleTest { + + @Autowired + private MediaArticleRepository mediaArticleRepository; + + @Test + void checkInitialLoad() { + assertTrue(mediaArticleRepository.findAll().isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaArticle mediaArticle = new MediaArticle(); + assertNull(mediaArticle.getUrl()); + assertNull(mediaArticle.getTitle()); + assertNull(mediaArticle.getCreatedDate()); + assertNull(mediaArticle.getLastModifiedDate()); + assertNull(mediaArticle.getId()); + assertFalse(mediaArticle.isReview()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java new file mode 100644 index 0000000..6ca61ce --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaFileTest.java @@ -0,0 +1,37 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +public class MediaFileTest { + + @Autowired + private MediaFileRepository mediaFileRepository; + + @Test + void checkInitialDataLoad() { + List mediaFileList = mediaFileRepository.findAll(); + assertTrue(mediaFileList.isEmpty()); + } + + @Test + void checkDefaultValues() { + MediaFile mediaFile = new MediaFile(); + assertNull(mediaFile.getUrl()); + assertNull(mediaFile.getTitle()); + assertNull(mediaFile.getFileName()); + assertNull(mediaFile.getPath()); + assertNull(mediaFile.getCloudLink()); + assertNull(mediaFile.getCreatedDate()); + assertNull(mediaFile.getLastModifiedDate()); + assertNull(mediaFile.getId()); + assertFalse(mediaFile.isReview()); + assertFalse(mediaFile.isShouldDownload()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java new file mode 100644 index 0000000..1a40335 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/data/MediaVideoTest.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.media.data; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@SpringBootTest +public class MediaVideoTest { + + @Autowired + private MediaVideoRepository mediaVideoRepository; + + @Test + @Order(1) + void checkInitialLoad() { + assertTrue(mediaVideoRepository.findAll().isEmpty()); + } + + @Test + @Order(2) + void checkDefaultValues() { + MediaVideo mediaVideo = new MediaVideo(); + assertNull(mediaVideo.getUrl()); + assertNull(mediaVideo.getTitle()); + assertNull(mediaVideo.getFileName()); + assertNull(mediaVideo.getPath()); + assertNull(mediaVideo.getCloudLink()); + assertNull(mediaVideo.getCreatedDate()); + assertNull(mediaVideo.getLastModifiedDate()); + assertNull(mediaVideo.getId()); + assertFalse(mediaVideo.isReview()); + assertFalse(mediaVideo.isShouldDownload()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java new file mode 100644 index 0000000..6178aa8 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaArticleServiceTest.java @@ -0,0 +1,50 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class MediaArticleServiceTest { + + @Autowired + MediaArticleService mediaArticleService; + + @Test + @Order(1) + void findAllMediaArticles() { + assertTrue(mediaArticleService.findAllMediaArticles(null).isEmpty()); + } + + @Test + @Order(2) + void saveMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + MediaArticle mediaArticle = new MediaArticle(); + mediaArticle.setUrl(TestConstants.URL); + mediaArticleService.saveMediaArticle(mediaArticle); + assertEquals(++mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } + + @Test + @Order(3) + void deleteMediaArticle() { + int mediaArticleCount = mediaArticleService.findAllMediaArticles(null).size(); + List mediaArticleList = mediaArticleService.findAllMediaArticles(TestConstants.URL); + assertEquals(1, mediaArticleService.findAllMediaArticles(null).size()); + mediaArticleService.deleteMediaArticle(mediaArticleList.get(0)); + assertEquals(--mediaArticleCount, mediaArticleService.findAllMediaArticles(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java new file mode 100644 index 0000000..2201be5 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaFileServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaFile; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaFileServiceTest { + + @Autowired + private MediaFileService mediaFileService; + + @Test + @Order(1) + void testFindAllMediaFiles() { + assertTrue(mediaFileService.findAllMediaFiles(null).isEmpty()); + } + + @Test + @Order(2) + void testSaveMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + MediaFile mediaFile = new MediaFile(); + mediaFile.setUrl(TestConstants.URL); + mediaFileService.saveMediaFile(mediaFile); + assertEquals(mediaFileCount +1, mediaFileService.findAllMediaFiles(null).size()); + } + + @Test + @Order(3) + void testDeleteMediaFile() { + int mediaFileCount = mediaFileService.findAllMediaFiles(null).size(); + List mediaFileList = mediaFileService.findAllMediaFiles(TestConstants.URL); + assertEquals(1, mediaFileList.size()); + mediaFileService.deleteMediaFile(mediaFileList.get(0)); + assertEquals(--mediaFileCount, mediaFileService.findAllMediaFiles(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java new file mode 100644 index 0000000..36a6fb1 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/media/services/MediaVideoServiceTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.media.services; + +import de.thpeetz.kontor.media.TestConstants; +import de.thpeetz.kontor.media.data.MediaArticle; +import de.thpeetz.kontor.media.data.MediaVideo; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Slf4j +@SpringBootTest +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MediaVideoServiceTest { + + @Autowired + private MediaVideoService mediaVideoService; + + @Test + void findAllMediaVideos() { + assertTrue(mediaVideoService.findAllMediaVideos(null).isEmpty()); + } + + @Test + void saveMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + MediaVideo mediaVideo = new MediaVideo(); + mediaVideo.setUrl(TestConstants.URL); + mediaVideoService.saveMediaVideo(mediaVideo); + assertEquals(++mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } + + @Test + void deleteMediaVideo() { + int mediaVideoCount = mediaVideoService.findAllMediaVideos(null).size(); + List mediaVideoList = mediaVideoService.findAllMediaVideos(TestConstants.URL); + assertEquals(1, mediaVideoList.size()); + mediaVideoService.deleteMediaVideo(mediaVideoList.get(0)); + assertEquals(--mediaVideoCount, mediaVideoService.findAllMediaVideos(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java new file mode 100644 index 0000000..a05c1dd --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/TestConstants.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.tysc; + +public class TestConstants { + + public static final Integer CARD_COUNT = 10; + public static final Integer CARD_CARDNUMBER = 999; + public static final Integer CARD_YEAR = 2000; + public static final Integer CARDSET_COUNT = 15; + public static final Integer FOOTBALL_TEAM_COUNT = 33; + public static final Integer FOOTBALL_POSITION_COUNT = 25; + public static final Integer PLAYER_COUNT = 38; + public static final Integer PLAYER_CHRIS_COUNT = 3; + public static final Integer POSITION_COUNT = 44; + public static final Integer POSITION_CENTER_COUNT = 3; + public static final Integer ROOSTER_COUNT = 11; + public static final Integer ROOSTER_YEAR = 1900; + public static final Integer SPORT_COUNT = 4; + public static final Integer TEAM_COUNT = 122; + public static final Integer VENDOR_COUNT = 9; + public static final String CARDSET_NAME = "CardSet"; + public static final String CARDSET_MYSTIQUE_NAME = "Mystique"; + public static final String DOLPHINS_NAME = "Miami Dolphins"; + public static final Object DOLPHINS_SHORT = "Dolphins"; + public static final String FOOTBALL_NAME = "Football"; + public static final String PLAYER_FIRSTNAME = "FirstName"; + public static final String PLAYER_LASTNAME = "LastName"; + public static final String PLAYER_CHRIS_NAME = "Chris"; + public static final String POSITION_CENTER = "Center"; + public static final String POSITION_NAME = "Position"; + public static final String POSITION_SHORTNAME = "POS"; + public static final String QUARTERBACK_NAME = "Quarterback"; + public static final String QUARTERBACK_SHORTNAME = "QB"; + public static final String SPORT_NAME = "Sport"; + public static final String TEAM_NAME = "Musterstadt Team"; + public static final String TEAM_SHORTNAME = "Team"; + public static final String TOPPS_NAME = "Topps"; + public static final String VENDOR_FLEER = "Fleer"; + public static final String VENDOR_NAME = "Vendor"; +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java new file mode 100644 index 0000000..a5699d6 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardSetTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardSetTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java new file mode 100644 index 0000000..0d71d8d --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/CardTest.java @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.services.CardService; + +@SpringBootTest +class CardTest { + + @Autowired + private CardService cardService; + + @Test + void checkInitialDataLoad() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java new file mode 100644 index 0000000..0853371 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/FieldPositionTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.thpeetz.kontor.tysc.repository.FieldPositionRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Test + void checkInitialDataLoad() { + assertEquals(Integer.toUnsignedLong(TestConstants.POSITION_COUNT), fieldPositionRepository.count()); + } + + @Test + void throwExceptionWhenPositionSavedWithEmptyName() { + FieldPosition fieldPosition = new FieldPosition(); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + fieldPosition.setName("Position"); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } + + @Test + void throwExceptionWhenPositionSavedWithExistingName() { + FieldPosition existingFieldPosition = fieldPositionRepository.findAll().get(11); + FieldPosition fieldPosition = new FieldPosition(); + fieldPosition.setName(existingFieldPosition.getName()); + assertThrows(TransactionSystemException.class, () -> { + fieldPositionRepository.save(fieldPosition); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java new file mode 100644 index 0000000..4941f6f --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/PlayerTest.java @@ -0,0 +1,54 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.PlayerRepository; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class PlayerTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + @Order(1) + void checkInitialDataLoad() { + List players = playerRepository.findAll(); + assertEquals(TestConstants.PLAYER_COUNT, players.size()); + } + + @Test + @Order(2) + void throwExceptionWhenPlayerSavedWithEmptyName() { + Player player = new Player(); + assertThrows(TransactionSystemException.class, () -> { + playerRepository.save(player); + }); + } + + @Test + @Order(3) + void throwExceptionWhenPlayerSavedWithExistingName() { + Player existingPlayer = playerRepository.findAll().get(10); + assertNotNull(existingPlayer); + Player player = new Player(); + player.setFirstName(existingPlayer.getFirstName()); + player.setLastName(existingPlayer.getLastName()); + assertThrows(DataIntegrityViolationException.class, () -> { + playerRepository.save(player); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java new file mode 100644 index 0000000..c0e2e6c --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/RoosterTest.java @@ -0,0 +1,48 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.RoosterRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class RoosterTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void checkInitialDataLoad() { + List roosters = roosterRepository.findAll(); + assertEquals(TestConstants.ROOSTER_COUNT, roosters.size()); + } + + @Test + void checkFetchingData() { + Rooster rooster = roosterRepository.findAll().get(4); + assertNotNull(rooster.getTeam()); + assertNotNull(rooster.getPlayer()); + assertNotNull(rooster.getPosition()); + } + + @Test + void exceptionThrownWhenSavingDuplicateRooster() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(5); + Rooster rooster = new Rooster(); + rooster.setPlayer(existingRooster.getPlayer()); + rooster.setTeam(existingRooster.getTeam()); + rooster.setPosition(existingRooster.getPosition()); + rooster.setYear(existingRooster.getYear()); + assertThrows(DataIntegrityViolationException.class, () -> { + roosterRepository.save(rooster); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java new file mode 100644 index 0000000..c045d12 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/SportTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.SportRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void checkInitialDataLoad() { + List sports = sportRepository.findAll(); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + } + + @Test + void exceptionThrownWhenSavingSportWithEmptyName() { + Sport sport = new Sport(); + assertThrows(TransactionSystemException.class, () -> { + sportRepository.save(sport); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Sport existingSport = sportRepository.findAll().get(0); + Sport sport = new Sport(); + sport.setName(existingSport.getName()); + assertThrows(DataIntegrityViolationException.class, () -> { + sportRepository.save(sport); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java new file mode 100644 index 0000000..d183b48 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/TeamTest.java @@ -0,0 +1,54 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.TeamRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void checkInitialDataLoad() { + List teams = teamRepository.findAll(); + assertEquals(TestConstants.TEAM_COUNT, teams.size()); + } + + @Test + void throwExceptionWhenTeamSavedWithEmptyName() { + Team team = new Team(); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithoutSport() { + Team team = new Team(); + team.setName(TestConstants.TEAM_NAME); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } + + @Test + void throwExceptionWhenTeamSavedWithExistingName() { + Team existingTeam = teamRepository.findAll().get(11); + Team team = new Team(); + team.setName(existingTeam.getName()); + team.setSport(existingTeam.getSport()); + assertThrows(TransactionSystemException.class, () -> { + teamRepository.save(team); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java new file mode 100644 index 0000000..ea63c11 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/data/VendorTest.java @@ -0,0 +1,45 @@ +package de.thpeetz.kontor.tysc.data; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.List; + +import de.thpeetz.kontor.tysc.repository.VendorRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.transaction.TransactionSystemException; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void checkInitialDataLoad() { + List vendors = vendorRepository.findAll(); + assertEquals(TestConstants.VENDOR_COUNT, vendors.size()); + } + + @Test + void exceptionThrownWhenSavingVendortWithEmptyName() { + Vendor vendor = new Vendor(); + assertThrows(TransactionSystemException.class, () -> { + vendorRepository.save(vendor); + }); + } + + @Test + void exceptionThrownWhenSavingSportWithExistingName() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.TOPPS_NAME); + assertThrows(DataIntegrityViolationException.class, () -> { + vendorRepository.save(vendor); + }); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardRepositoryTest.java new file mode 100644 index 0000000..96f9f45 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardRepositoryTest.java @@ -0,0 +1,39 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CardRepositoryTest { + + @Autowired + private CardRepository cardRepository; + + @Test + void testSearchByFields() { + List cards = cardRepository.findAll(); + Card existingCard = cards.get(3); + Vendor vendor = existingCard.getVendor(); + CardSet cardSet = existingCard.getCardSet(); + Rooster rooster = existingCard.getRooster(); + int cardNumber = existingCard.getCardNumber(); + int year = existingCard.getYear(); + Card card = cardRepository.search(vendor, cardSet, rooster, cardNumber, year); + assertNotNull(card); + } + + @Test + void testSearch() { + assertEquals(2, cardRepository.search("2").size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardSetRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardSetRepositoryTest.java new file mode 100644 index 0000000..2790087 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/CardSetRepositoryTest.java @@ -0,0 +1,44 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Vendor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class CardSetRepositoryTest { + + @Autowired + private CardSetRepository cardSetRepository; + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + List cardSets = cardSetRepository.findByName(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(1, cardSets.size()); + } + + @Test + void testFindByNameAndVendor() { + Vendor fleer = vendorRepository.findByName(TestConstants.VENDOR_FLEER); + assertNotNull(fleer); + CardSet cardSet = cardSetRepository.findByNameAndVendor(TestConstants.CARDSET_MYSTIQUE_NAME, fleer); + assertNotNull(cardSet); + } + + @Test + void testSearch() { + List cardSets = cardSetRepository.search("SP"); + assertEquals(3, cardSets.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepositoryTest.java new file mode 100644 index 0000000..3c5e26e --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/FieldPositionRepositoryTest.java @@ -0,0 +1,61 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Sport; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class FieldPositionRepositoryTest { + + @Autowired + private FieldPositionRepository fieldPositionRepository; + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByShortName() { + FieldPosition position = fieldPositionRepository.findByShortName(TestConstants.QUARTERBACK_SHORTNAME); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameAndSport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + FieldPosition position = fieldPositionRepository.findByShortNameAndSport(TestConstants.QUARTERBACK_SHORTNAME, sport); + assertNotNull(position); + assertEquals(TestConstants.QUARTERBACK_NAME, position.getName()); + } + + @Test + void testFindByShortNameIgnoreCase() { + List positions = fieldPositionRepository + .findByShortNameIgnoreCase(TestConstants.QUARTERBACK_SHORTNAME.toLowerCase()); + assertNotNull(positions); + assertEquals(1, positions.size()); + assertEquals(TestConstants.QUARTERBACK_NAME, positions.get(0).getName()); + } + + @Test + void testFindBySport() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + List positions = fieldPositionRepository.findBySport(sport); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, positions.size()); + } + + @Test + void testSearch() { + List positions = fieldPositionRepository.search("back"); + assertEquals(8, positions.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/PlayerRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/PlayerRepositoryTest.java new file mode 100644 index 0000000..1f602c7 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/PlayerRepositoryTest.java @@ -0,0 +1,32 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Player; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class PlayerRepositoryTest { + + @Autowired + private PlayerRepository playerRepository; + + @Test + void testFindByFirstNameAndLastName() { + Player existingPlayer = playerRepository.findAll().get(11); + String firstName = existingPlayer.getFirstName(); + String lastName = existingPlayer.getLastName(); + Player found = playerRepository.findByFirstNameAndLastName(firstName, lastName); + assertEquals(existingPlayer.getFullName(), found.getFullName()); + } + + @Test + void testSearch() { + List players = playerRepository.search("can"); + assertEquals(1, players.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/RoosterRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/RoosterRepositoryTest.java new file mode 100644 index 0000000..a310edb --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/RoosterRepositoryTest.java @@ -0,0 +1,33 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Team; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RoosterRepositoryTest { + + @Autowired + private RoosterRepository roosterRepository; + + @Test + void testFindByReferences() { + List roosters = roosterRepository.findAll(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = roosterRepository.findByReferences(player, team, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/SportRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/SportRepositoryTest.java new file mode 100644 index 0000000..d11fd07 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/SportRepositoryTest.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Sport; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class SportRepositoryTest { + + @Autowired + private SportRepository sportRepository; + + @Test + void testFindByName() { + Sport sport = sportRepository.findByName(TestConstants.FOOTBALL_NAME); + assertNotNull(sport); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sport.getTeams().size()); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sport.getPositions().size()); + } + + @Test + void testFindByNameIgnoreCase() { + List sports = sportRepository.findByNameIgnoreCase(TestConstants.FOOTBALL_NAME.toLowerCase()); + assertNotNull(sports); + assertEquals(TestConstants.FOOTBALL_TEAM_COUNT, sports.get(0).getTeams().size()); + } + + @Test + void testSearch() { + List sports = sportRepository.search("ball"); + assertEquals(3, sports.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/TeamRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/TeamRepositoryTest.java new file mode 100644 index 0000000..83061a3 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/TeamRepositoryTest.java @@ -0,0 +1,38 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Team; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class TeamRepositoryTest { + + @Autowired + private TeamRepository teamRepository; + + @Test + void testFindByName() { + Team team = teamRepository.findByName(TestConstants.DOLPHINS_NAME); + assertEquals(TestConstants.DOLPHINS_SHORT, team.getShortName()); + } + + @Test + void testFindByNameIgnoreCase() { + List teams = teamRepository.findByNameIgnoreCase(TestConstants.DOLPHINS_NAME.toLowerCase()); + assertEquals(1, teams.size()); + assertEquals(TestConstants.DOLPHINS_SHORT, teams.get(0).getShortName()); + } + + @Test + void testSearch() { + List teams = teamRepository.search("dol"); + assertEquals(1, teams.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/VendorRepositoryTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/VendorRepositoryTest.java new file mode 100644 index 0000000..ef905c8 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/repository/VendorRepositoryTest.java @@ -0,0 +1,42 @@ +package de.thpeetz.kontor.tysc.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.Vendor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; + +@SpringBootTest +class VendorRepositoryTest { + + @Autowired + private VendorRepository vendorRepository; + + @Test + void testFindByName() { + Vendor vendor = vendorRepository.findByName(TestConstants.TOPPS_NAME); + assertNotNull(vendor); + assertEquals(TestConstants.TOPPS_NAME, vendor.getName()); + } + + @Test + void testFindByNameIgnoreCase() { + List vendors = vendorRepository.findByNameIgnoreCase(TestConstants.TOPPS_NAME.toLowerCase()); + assertNotNull(vendors); + assertEquals(1, vendors.size()); + assertEquals(TestConstants.TOPPS_NAME, vendors.get(0).getName()); + } + + @Test + void testSearch() { + List vendors = vendorRepository.search("pp"); + assertNotNull(vendors); + assertEquals(2, vendors.size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java new file mode 100644 index 0000000..e215217 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/CardServiceTest.java @@ -0,0 +1,126 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Card; +import de.thpeetz.kontor.tysc.data.CardSet; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Vendor; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class CardServiceTest { + + @Autowired + private CardService cardService; + + @Autowired + private SportService sportService; + + @Test + @Order(1) + void testFindAllVendors() { + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + assertEquals(2, cardService.findAllVendors("pp").size()); + } + + @Test + @Order(2) + void testSaveVendor() { + Vendor vendor = new Vendor(); + vendor.setName(TestConstants.VENDOR_NAME); + vendor = cardService.saveVendor(vendor); + assertNotNull(vendor); + assertEquals(TestConstants.VENDOR_COUNT + 1, cardService.findAllVendors(null).size()); + } + + @Test + @Order(3) + void testDeleteVendor() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_NAME); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + cardService.deleteVendor(vendor); + assertEquals(TestConstants.VENDOR_COUNT, cardService.findAllVendors(null).size()); + } + + @Test + @Order(4) + void testFindAllCardSets() { + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + assertEquals(1, cardService.findAllCardSets("Ultra").size()); + } + + @Test + @Order(5) + void testSaveCardSet() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + CardSet cardSet = new CardSet(); + cardSet.setName(TestConstants.CARDSET_NAME); + cardSet.setVendor(vendor); + cardSet.setInsertSet(false); + cardSet.setParallelSet(false); + cardService.saveCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT + 1, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(6) + void testDeleteCardSet() { + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_NAME); + assertEquals(1, cardSets.size()); + CardSet cardSet = cardSets.get(0); + cardService.deleteCardSet(cardSet); + assertEquals(TestConstants.CARDSET_COUNT, cardService.findAllCardSets(null).size()); + } + + @Test + @Order(7) + void testFindAllCards() { + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + assertEquals(1, cardService.findAllCards("112").size()); + } + + @Test + @Order(8) + void testSaveCard() { + List vendors = cardService.findAllVendors(TestConstants.VENDOR_FLEER); + assertEquals(1, vendors.size()); + Vendor vendor = vendors.get(0); + List cardSets = cardService.findAllCardSets(TestConstants.CARDSET_MYSTIQUE_NAME); + assertEquals(3, cardSets.size()); + CardSet cardSet = cardSets.get(0); + List roosters = sportService.findAllRoosters(); + Rooster rooster = roosters.get(0); + Card card = new Card(); + card.setCardNumber(TestConstants.CARD_CARDNUMBER); + card.setCardSet(cardSet); + card.setVendor(vendor); + card.setRooster(rooster); + card.setYear(TestConstants.CARD_YEAR); + cardService.saveCard(card); + assertEquals(TestConstants.CARD_COUNT + 1, cardService.findAllCards(null).size()); + } + + @Test + @Order(9) + void testDeleteCard() { + List cards = cardService.findAllCards(TestConstants.CARD_CARDNUMBER.toString()); + assertEquals(1, cards.size()); + Card card = cards.get(0); + cardService.deleteCard(card); + assertEquals(TestConstants.CARD_COUNT, cardService.findAllCards(null).size()); + } +} diff --git a/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java new file mode 100644 index 0000000..30b7ed6 --- /dev/null +++ b/kontor-spring/src/test/java/de/thpeetz/kontor/tysc/services/SportServiceTest.java @@ -0,0 +1,200 @@ +package de.thpeetz.kontor.tysc.services; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import de.thpeetz.kontor.tysc.data.FieldPosition; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import de.thpeetz.kontor.tysc.TestConstants; +import de.thpeetz.kontor.tysc.data.Player; +import de.thpeetz.kontor.tysc.data.Rooster; +import de.thpeetz.kontor.tysc.data.Sport; +import de.thpeetz.kontor.tysc.data.Team; + +@SpringBootTest +@TestMethodOrder(OrderAnnotation.class) +class SportServiceTest { + + @Autowired + private SportService sportService; + + + @Test + @Order(1) + void testFindAllSports() { + List sports = sportService.findAllSports(null); + assertEquals(TestConstants.SPORT_COUNT, sports.size()); + assertEquals(3, sportService.findAllSports("ball").size()); + } + + @Test + @Order(2) + void testSaveSport() { + Sport sport = new Sport(); + sport.setName(TestConstants.SPORT_NAME); + sport = sportService.saveSport(sport); + assertNotNull(sport); + assertEquals(TestConstants.SPORT_COUNT+1, sportService.findAllSports(null).size()); + } + + @Test + @Order(3) + void testDeleteSport() { + List sports = sportService.findAllSports(TestConstants.SPORT_NAME); + assertEquals(1, sports.size()); + Sport sport = sports.get(0); + sportService.deleteSport(sport); + assertEquals(TestConstants.SPORT_COUNT, sportService.findAllSports(null).size()); + } + + @Test + @Order(4) + void testFindAllPlayers() { + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + assertEquals(TestConstants.PLAYER_CHRIS_COUNT, sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).size()); + } + + @Test + @Order(5) + void testSavePlayer() { + Player player = new Player(); + player.setFirstName(TestConstants.PLAYER_FIRSTNAME); + player.setLastName(TestConstants.PLAYER_LASTNAME); + player = sportService.savePlayer(player); + assertNotNull(player); + } + + @Test + @Order(6) + void testDeletePlayer() { + List players = sportService.findAllPlayers(TestConstants.PLAYER_LASTNAME); + assertEquals(1, players.size()); + Player player = players.get(0); + sportService.deletePlayer(player); + assertEquals(TestConstants.PLAYER_COUNT, sportService.findAllPlayers(null).size()); + } + + + @Test + @Order(7) + void testFindAllPositions() { + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + assertEquals(TestConstants.POSITION_CENTER_COUNT, sportService.findAllPositions(TestConstants.POSITION_CENTER).size()); + } + + @Test + @Order(8) + void testFindAllPositionsForSport() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + assertEquals(1, sports.size()); + Sport football = sports.get(0); + assertEquals(TestConstants.FOOTBALL_POSITION_COUNT, sportService.findAllPositionsForSport(football).size()); + } + + @Test + @Order(9) + void testSavePosition() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + FieldPosition position = new FieldPosition(); + position.setSport(football); + position.setName(TestConstants.POSITION_NAME); + position.setShortName(TestConstants.POSITION_SHORTNAME); + sportService.savePosition(position); + assertEquals(TestConstants.POSITION_COUNT+1, sportService.findAllPositions(null).size()); + } + + @Test + @Order(10) + void testDeletePosition() { + List positions = sportService.findAllPositions(TestConstants.POSITION_NAME); + assertEquals(1, positions.size()); + FieldPosition position = positions.get(0); + sportService.deletePosition(position); + assertEquals(TestConstants.POSITION_COUNT, sportService.findAllPositions(null).size()); + } + + @Test + @Order(11) + void testFindAllTeams() { + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + assertEquals(1, sportService.findAllTeams("sharks").size()); + } + + @Test + @Order(12) + void testSaveTeam() { + List sports = sportService.findAllSports(TestConstants.FOOTBALL_NAME); + Sport football = sports.get(0); + Team team = new Team(); + team.setSport(football); + team.setName(TestConstants.TEAM_NAME); + team.setShortName(TestConstants.TEAM_SHORTNAME); + sportService.saveTeam(team); + assertEquals(TestConstants.TEAM_COUNT+1, sportService.findAllTeams(null).size()); + } + + @Test + @Order(13) + void testDeleteTeam() { + List teams = sportService.findAllTeams(TestConstants.TEAM_NAME); + assertEquals(1, teams.size()); + Team team = teams.get(0); + sportService.deleteTeam(team); + assertEquals(TestConstants.TEAM_COUNT, sportService.findAllTeams(null).size()); + } + + @Test + @Order(14) + void testFindAllRoosters() { + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } + + @Test + @Order(15) + void testFindRoosterByFields() { + List roosters = sportService.findAllRoosters(); + Rooster existingRooster = roosters.get(4); + Team team = existingRooster.getTeam(); + Player player = existingRooster.getPlayer(); + FieldPosition position = existingRooster.getPosition(); + int year = existingRooster.getYear(); + Rooster rooster = sportService.findRoosterByFields(team, player, position, year); + assertNotNull(rooster); + assertEquals(existingRooster.getId(), rooster.getId()); + } + + @Test + @Order(16) + void testSaveRooster() { + Rooster rooster = new Rooster(); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + rooster.setPlayer(player); + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + rooster.setTeam(team); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + rooster.setPosition(position); + rooster.setYear(TestConstants.ROOSTER_YEAR); + sportService.saveRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT+1, sportService.findAllRoosters().size()); + } + + @Test + @Order(17) + void testDeleteRooster() { + Team team = sportService.findAllTeams(TestConstants.DOLPHINS_NAME).get(0); + Player player = sportService.findAllPlayers(TestConstants.PLAYER_CHRIS_NAME).get(0); + FieldPosition position = sportService.findAllPositions(TestConstants.QUARTERBACK_NAME).get(0); + Rooster rooster = sportService.findRoosterByFields(team, player, position, TestConstants.ROOSTER_YEAR); + assertNotNull(rooster); + sportService.deleteRooster(rooster); + assertEquals(TestConstants.ROOSTER_COUNT, sportService.findAllRoosters().size()); + } +} diff --git a/kontor-spring/src/test/resources/application.properties b/kontor-spring/src/test/resources/application.properties new file mode 100644 index 0000000..53be2f4 --- /dev/null +++ b/kontor-spring/src/test/resources/application.properties @@ -0,0 +1,30 @@ +server.port=8085 + +spring.hibernate.dialect=org.hibernate.dialect.HSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.HSQLDialect +spring.datasource.driverClassName=org.hsqldb.jdbc.JDBCDriver +spring.datasource.url=jdbc:hsqldb:mem:testDb +spring.datasource.username=sa +spring.datasource.password=sa + +#spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect +#spring.datasource.driverClassName=org.sqlite.JDBC +#spring.datasource.url=jdbc:sqlite:file:./kontorTesrDb?cache=shared +#spring.datasource.username=sa +#spring.datasource.password=sa + +spring.jpa.defer-datasource-initialization = true +#spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false +spring.sql.init.mode=always + +spring.mustache.check-template-location = false + +logging.level.org.atmosphere=INFO +logging.level.org.springframework.web=INFO +logging.level.guru.springframework.controllers=DEBUG +logging.level.org.hibernate=INFO +logging.level.de.thpeetz=DEBUG + +jwt.auth.secret=J6GOtcwC2NJI1l0VkHu20PacPFGTxpirBxWwynoHjsc= diff --git a/kontor.tjp b/kontor.tjp new file mode 100644 index 0000000..a5d46c6 --- /dev/null +++ b/kontor.tjp @@ -0,0 +1,30 @@ +project kontor "Kontor" "0.1.0" 2024-12-05 +5m { + timezone "Europe/Berlin" + timeformat "%d.%m.%Y" + numberformat "-" "" "" "," 1 + currencyformat "-" "" "" "," 0 + currency "EUR" + + scenario plan "Plan" { + scenario real "Realität" + } +} + +resource gcpce "Google Cloud Compute Engine" { + efficiency 0.0 + rate 0.25 +} + +task flask "Kontor-Flask" { + task import "Import repository kontor-flask into directory flask" +} +task springboot "Springboot Vaadin" { + task import "Import repository kontor-spring into directory springboot" +} + +taskreport "Arbeitsliste" { + formats html + hidetask ~isleaf() + sorttasks plan.end.up +} + diff --git a/kotlin-quarkus/.dockerignore b/kotlin-quarkus/.dockerignore new file mode 100644 index 0000000..4361d2f --- /dev/null +++ b/kotlin-quarkus/.dockerignore @@ -0,0 +1,5 @@ +* +!build/*-runner +!build/*-runner.jar +!build/lib/* +!build/quarkus-app/* \ No newline at end of file diff --git a/kotlin-quarkus/.gitignore b/kotlin-quarkus/.gitignore new file mode 100644 index 0000000..285b6ba --- /dev/null +++ b/kotlin-quarkus/.gitignore @@ -0,0 +1,36 @@ +# Gradle +.gradle/ +build/ + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/kotlin-quarkus/README.md b/kotlin-quarkus/README.md new file mode 100644 index 0000000..08f2a76 --- /dev/null +++ b/kotlin-quarkus/README.md @@ -0,0 +1,78 @@ +# kontor Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./gradlew quarkusDev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./gradlew build +``` +It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./gradlew build -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./gradlew build -Dquarkus.package.type=native +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./gradlew build -Dquarkus.package.type=native -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./build/kontor-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/gradle-tooling. + +## Related Guides + +- JDBC Driver - H2 ([guide](https://quarkus.io/guides/datasource)): Connect to the H2 database via JDBC +- Hibernate ORM with Panache and Kotlin ([guide](https://quarkus.io/guides/hibernate-orm-panache-kotlin)): Define your persistent model in Hibernate ORM with Panache +- SmallRye OpenAPI ([guide](https://quarkus.io/guides/openapi-swaggerui)): Document your REST APIs with OpenAPI - comes with Swagger UI +- YAML Configuration ([guide](https://quarkus.io/guides/config#yaml)): Use YAML to configure your Quarkus application +- SmallRye Health ([guide](https://quarkus.io/guides/microprofile-health)): Monitor service health + +## Provided Code + +### YAML Config + +Configure your application with YAML + +[Related guide section...](https://quarkus.io/guides/config-reference#configuration-examples) + +The Quarkus application configuration is located in `src/main/resources/application.yml`. + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) + +### SmallRye Health + +Monitor your application's health using SmallRye Health + +[Related guide section...](https://quarkus.io/guides/smallrye-health) diff --git a/kotlin-quarkus/build.gradle.kts b/kotlin-quarkus/build.gradle.kts new file mode 100644 index 0000000..0a6ac3a --- /dev/null +++ b/kotlin-quarkus/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + java + id("io.quarkus") +} + +repositories { + mavenCentral() + mavenLocal() +} + +val quarkusPlatformGroupId: String by project +val quarkusPlatformArtifactId: String by project +val quarkusPlatformVersion: String by project + +dependencies { + implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}")) + implementation("io.quarkus:quarkus-resteasy-reactive-jackson") + implementation("io.quarkus:quarkus-jdbc-h2") + implementation("io.quarkus:quarkus-hibernate-orm-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-openapi") + implementation("io.quarkus:quarkus-config-yaml") + implementation("io.quarkus:quarkus-hibernate-reactive-panache-kotlin") + implementation("io.quarkus:quarkus-smallrye-health") + implementation("io.quarkus:quarkus-arc") + implementation("io.quarkus:quarkus-resteasy-reactive") + testImplementation("io.quarkus:quarkus-junit5") + testImplementation("io.rest-assured:rest-assured") +} + +group = "de.thpeetz.kontor" +version = "1.0.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType { + systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager") +} + +tasks.withType { + options.encoding = "UTF-8" + options.compilerArgs.add("-parameters") +} diff --git a/kotlin-quarkus/gradle.properties b/kotlin-quarkus/gradle.properties new file mode 100644 index 0000000..1057e5c --- /dev/null +++ b/kotlin-quarkus/gradle.properties @@ -0,0 +1,6 @@ +#Gradle properties +quarkusPluginId=io.quarkus +quarkusPluginVersion=2.15.2.Final +quarkusPlatformGroupId=io.quarkus.platform +quarkusPlatformArtifactId=quarkus-bom +quarkusPlatformVersion=2.15.2.Final \ No newline at end of file diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..62d4c053550b91381bbd28b1afc82d634bf73a8a GIT binary patch literal 58910 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 literal 0 HcmV?d00001 diff --git a/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/kotlin-quarkus/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kotlin-quarkus/gradlew b/kotlin-quarkus/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/kotlin-quarkus/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/kotlin-quarkus/gradlew.bat b/kotlin-quarkus/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/kotlin-quarkus/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kotlin-quarkus/settings.gradle.kts b/kotlin-quarkus/settings.gradle.kts new file mode 100644 index 0000000..043f9fc --- /dev/null +++ b/kotlin-quarkus/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + val quarkusPluginVersion: String by settings + val quarkusPluginId: String by settings + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } + plugins { + id(quarkusPluginId) version quarkusPluginVersion + } +} +rootProject.name="kontor" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.jvm b/kotlin-quarkus/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..669d901 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/kontor-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..ab8ff1f --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/kontor-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/kontor-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native b/kotlin-quarkus/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..38e4141 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/docker/Dockerfile.native-micro b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..cce62e8 --- /dev/null +++ b/kotlin-quarkus/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.type=native +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/kontor . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/kontor +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/kotlin-quarkus/src/main/resources/META-INF/resources/index.html b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..f64aa10 --- /dev/null +++ b/kotlin-quarkus/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,289 @@ + + + + + kontor - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.yml

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • JDBC Driver - H2 (guide)
  • +
  • Hibernate ORM with Panache and Kotlin (guide)
  • +
  • SmallRye OpenAPI (guide)
  • +
  • YAML Configuration (guide)
  • +
  • Hibernate Reactive Panache Kotlin
  • +
  • SmallRye Health (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/kotlin-quarkus/src/main/resources/application.yml b/kotlin-quarkus/src/main/resources/application.yml new file mode 100644 index 0000000..527a35f --- /dev/null +++ b/kotlin-quarkus/src/main/resources/application.yml @@ -0,0 +1,2 @@ +greeting: + message: "hello" diff --git a/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java new file mode 100644 index 0000000..ef71448 --- /dev/null +++ b/kotlin-quarkus/src/native-test/java/de/thpeetz/kontor/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package de.thpeetz.kontor; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/kotlin-spring/.gitignore b/kotlin-spring/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/kotlin-spring/.gitignore @@ -0,0 +1,37 @@ +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 +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/kotlin-spring/README.md b/kotlin-spring/README.md new file mode 100644 index 0000000..0473319 --- /dev/null +++ b/kotlin-spring/README.md @@ -0,0 +1,92 @@ +# Kontor Kotlin + + + +## Getting started + +To make it easy for you to get started with GitLab, here's a list of recommended next steps. + +Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +``` +cd existing_repo +git remote add origin https://gitlab.thpeetz.de/kontor/kontor-kotlin.git +git branch -M main +git push -uf origin main +``` + +## Integrate with your tools + +- [ ] [Set up project integrations](https://gitlab.thpeetz.de/kontor/kontor-kotlin/-/settings/integrations) + +## Collaborate with your team + +- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) +- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) +- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) +- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) +- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) + +## Test and Deploy + +Use the built-in continuous integration in GitLab. + +- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) +- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) +- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) +- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) +- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) + +*** + +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Name +Choose a self-explaining name for your project. + +## Description +Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/kotlin-spring/build.gradle b/kotlin-spring/build.gradle new file mode 100644 index 0000000..c6df720 --- /dev/null +++ b/kotlin-spring/build.gradle @@ -0,0 +1,58 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id 'org.springframework.boot' version '2.7.7' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.spring' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.jpa' version '1.7.21' + id 'org.jetbrains.kotlin.plugin.allopen' version '1.7.21' + id 'org.jetbrains.kotlin.kapt' version '1.7.21' +} + +group = 'de.thpeetz.kontor' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = '11' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-mustache' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + kapt 'org.springframework.boot:spring-boot-configuration-processor' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'org.springframework.boot:spring-boot-devtools' + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + exclude module: 'mockito-core' + } + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testImplementation 'com.ninja-squad:springmockk:3.1.1' + + +} + +tasks.withType(KotlinCompile) { + kotlinOptions { + freeCompilerArgs = ['-Xjsr305=strict'] + jvmTarget = '11' + } +} + +tasks.named('test') { + useJUnitPlatform() +} + +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.Embeddable") + annotation("javax.persistence.MappedSuperclass") +} diff --git a/kotlin-spring/gradle/wrapper/gradle-wrapper.jar b/kotlin-spring/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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/kotlin-spring/gradlew.bat b/kotlin-spring/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/kotlin-spring/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% 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/kotlin-spring/settings.gradle b/kotlin-spring/settings.gradle new file mode 100644 index 0000000..185aa4e --- /dev/null +++ b/kotlin-spring/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kontor' diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt new file mode 100644 index 0000000..279b7db --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorApplication.kt @@ -0,0 +1,16 @@ +package de.thpeetz.kontor + +import org.springframework.boot.Banner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.boot.runApplication + +@SpringBootApplication +@EnableConfigurationProperties(KontorProperties::class) +class KontorApplication + +fun main(args: Array) { + runApplication(*args) { + setBannerMode(Banner.Mode.OFF) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt new file mode 100644 index 0000000..e54f2f9 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorConfiguration.kt @@ -0,0 +1,79 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.comics.* +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class KontorConfiguration { + @Bean + fun databaseInitializer(artistRepository: ArtistRepository, + publisherRepository: PublisherRepository, + comicRepository: ComicRepository, + issueRepository: IssueRepository + ) = ApplicationRunner { + + artistRepository.save(Artist("Turner, Michael")) + artistRepository.save(Artist("Bendis, Brian Michael")) + artistRepository.save(Artist("Land, Greg")) + artistRepository.save(Artist("Whedon, Joss")) + val marvel = publisherRepository.save(Publisher("Marvel")) + val aspen = publisherRepository.save(Publisher("Aspen")) + publisherRepository.save(Publisher("DC")) + val de = publisherRepository.save(Publisher("Dynamite Entertainment")) + val wildstorm = publisherRepository.save(Publisher("WildStorm")) + val bongo = publisherRepository.save(Publisher("Bongo")) + val image = publisherRepository.save(Publisher("Image")) + val darkHorse = publisherRepository.save(Publisher("Dark Horse Comics")) + val cliffhanger = publisherRepository.save(Publisher("Cliffhanger")) + comicRepository.save(Comic(title = "X-Men", publisher = marvel, currentOrder = false, completed = false)) + val redSonja = comicRepository.save(Comic(title = "Red Sonja", publisher = de, currentOrder = false, completed = false)) + val x23 = comicRepository.save(Comic(title = "X-23", publisher = marvel, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Simpsons Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Futurama Comics", publisher = bongo, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen III: The Good, The Bad and The Lovely", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Bomb Queen IV: Suicide Bomber", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Gen13", publisher = wildstorm, currentOrder = false, completed = false)) + val bombqueen = comicRepository.save(Comic(title = "Bomb Queen II: Queen of Hearts", publisher = image, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Iron & The Maiden",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Fathom",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Soulfire",publisher = aspen,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Rebellion",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Knights of the Old Republic",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Legacy",publisher = darkHorse,currentOrder = false,completed = false)) + comicRepository.save(Comic(title = "Star Wars: Dark Times", publisher = darkHorse, currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Samurai: Heaven and Earth", publisher = darkHorse, currentOrder = false, completed = false)) + val battlpope = comicRepository.save(Comic(title = "Battle Pope", publisher = image,currentOrder = false, completed = false)) + comicRepository.save(Comic(title = "Danger Girl", publisher = cliffhanger, currentOrder = false, completed = false)) + val marville = comicRepository.save(Comic(title = "Marville", publisher = marvel, currentOrder = false, completed = false)) + issueRepository.save(Issue(number = "0", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "2", comic = redSonja, gelesen = true)) + issueRepository.save(Issue(number = "1", comic = x23, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = bombqueen, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "8", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "9", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "10", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "11", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "12", comic = battlpope, gelesen = false)) + issueRepository.save(Issue(number = "1", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "2", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "3", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "4", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "5", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "6", comic = marville, gelesen = false)) + issueRepository.save(Issue(number = "7", comic = marville, gelesen = false)) + } +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt new file mode 100644 index 0000000..3d639dd --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorController.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/") +class KontorController(val properties: KontorProperties, val navigation: Navigation) { + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Kontor" + model["banner"] = properties.banner + model["version"] = properties.version + model["navigation"] = navigation.links() + model["login"] = navigation.login() + return "kontor" + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt new file mode 100644 index 0000000..92c77c7 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/KontorProperties.kt @@ -0,0 +1,10 @@ +package de.thpeetz.kontor + +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.ConstructorBinding + +@ConstructorBinding +@ConfigurationProperties("kontor") +data class KontorProperties(var title: String, var version: String = "unknown", val banner: Banner) { + data class Banner(val title: String? = null, val content: String) +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt new file mode 100644 index 0000000..fb730af --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/Navigation.kt @@ -0,0 +1,24 @@ +package de.thpeetz.kontor + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class Navigation { + + @Bean + fun links(): Iterable = listOf( + Link("/comics", "Comics"), + Link("/library","Library"), + Link("/office", "HomeOffice"), + Link("/tradingcards", "Trading Cards"), + Link("/tysc","TradeYourSportsCards"), + Link("/user/login", ""), + Link("/admin/","Admin") + ) + + @Bean + fun login(): Iterable = listOf(Link("/user/login", "Login")) +} + +data class Link(val link: String, val title: String) diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt new file mode 100644 index 0000000..decb70f --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/RequestLoggingFilter.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor + +import org.slf4j.LoggerFactory +import javax.servlet.Filter +import org.springframework.stereotype.Component +import javax.servlet.FilterChain +import javax.servlet.ServletRequest +import javax.servlet.ServletResponse + +@Component +class RequestLoggingFilter: Filter { + val loggerFactory = LoggerFactory.getLogger("Kontor Logger") + + override fun doFilter( + request: ServletRequest, + response: ServletResponse, + filterChain: FilterChain + ) { + val requestString = request.servletContext.contextPath.toString() + //val attributeNames = request.attributeNames + //attributeNames.toList().forEach { loggerFactory.info("Attribute: ${it.toString()}") } + loggerFactory.info("Logging request: $requestString") + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt new file mode 100644 index 0000000..d2e3b4e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsApiController.kt @@ -0,0 +1,25 @@ +package de.thpeetz.kontor.comics + +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("api/") +class ComicsApiController(val artistRepository: ArtistRepository, + val publisherRepository: PublisherRepository, + val comicRepository: ComicRepository, + val issueRepository: IssueRepository) { + + @GetMapping("/artist") + fun artistList() = artistRepository.findAll() + + @GetMapping("/publisher") + fun publisherList() = publisherRepository.findAll() + + @GetMapping("/comics") + fun comicsList() = comicRepository.findAll() + + @GetMapping("/issues") + fun issuesList() = issueRepository.findAll() +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt new file mode 100644 index 0000000..f339b04 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/ComicsController.kt @@ -0,0 +1,29 @@ +package de.thpeetz.kontor.comics + +import de.thpeetz.kontor.KontorProperties +import de.thpeetz.kontor.Navigation +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.ui.set +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping + +@Controller +@RequestMapping("/comics") +class ComicsController(private val properties: KontorProperties, + val navigation: Navigation, + val comicRepository: ComicRepository +) { + + @GetMapping("/") + fun blog(model: Model): String { + model["title"] = properties.title + model["title"] = "Comics" + model["banner"] = properties.banner + model["navigation"] = navigation.links() + model["comics"] = comicRepository.findAll().map { it } + return "comics" + } + + +} diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt new file mode 100644 index 0000000..3be00f5 --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Entities.kt @@ -0,0 +1,36 @@ +package de.thpeetz.kontor.comics + +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.Id +import javax.persistence.ManyToOne + + +@Entity +class Artist( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Publisher( + val name: String, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Comic( + val title: String, + @ManyToOne val publisher: Publisher, + val currentOrder: Boolean, + val completed: Boolean, + @Id @GeneratedValue val id: Long? = null +) + +@Entity +class Issue( + val number: String, + @ManyToOne val comic: Comic, + val gelesen: Boolean, + @Id @GeneratedValue val id: Long? = null +) \ No newline at end of file diff --git a/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt new file mode 100644 index 0000000..fe44b2e --- /dev/null +++ b/kotlin-spring/src/main/kotlin/de/thpeetz/kontor/comics/Repositories.kt @@ -0,0 +1,22 @@ +package de.thpeetz.kontor.comics + +import org.springframework.data.repository.CrudRepository + +interface ArtistRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface PublisherRepository: CrudRepository { + override fun findAll(): Iterable +} + +interface ComicRepository: CrudRepository { + + override fun findAll(): Iterable +} + +interface IssueRepository: CrudRepository { + + override fun findAll(): Iterable +} \ No newline at end of file diff --git a/kotlin-spring/src/main/resources/application.properties b/kotlin-spring/src/main/resources/application.properties new file mode 100644 index 0000000..6d27d3a --- /dev/null +++ b/kotlin-spring/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.properties.hibernate.globally_quoted_identifiers_skip_column_definitions = true +kontor.title=Kontor +kontor.version=1.0.0-SNAPSHOT +kontor.banner.title=Warning +kontor.banner.content=The blog will be down tomorrow. diff --git a/kotlin-spring/src/main/resources/templates/comics.mustache b/kotlin-spring/src/main/resources/templates/comics.mustache new file mode 100644 index 0000000..336d9f0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/comics.mustache @@ -0,0 +1,23 @@ +{{> header}} + +{{> menu}} +

+ +
+ + + + {{#comics}} + + {{/comics}} +
List of Comics
Name
{{title}}
+
+ Add entry +
+
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/footer.mustache b/kotlin-spring/src/main/resources/templates/footer.mustache new file mode 100644 index 0000000..dde9847 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/footer.mustache @@ -0,0 +1,22 @@ + + + + diff --git a/kotlin-spring/src/main/resources/templates/header.mustache b/kotlin-spring/src/main/resources/templates/header.mustache new file mode 100644 index 0000000..d794c9e --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/header.mustache @@ -0,0 +1,15 @@ + + + + + {{title}} + + + + + + + + + + diff --git a/kotlin-spring/src/main/resources/templates/kontor.mustache b/kotlin-spring/src/main/resources/templates/kontor.mustache new file mode 100644 index 0000000..177306f --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/kontor.mustache @@ -0,0 +1,18 @@ +{{> header}} + +{{> menu}} + +
+ {{#banner.title}} +
+ + +
+ {{/banner.title}} +
+ +{{> footer}} diff --git a/kotlin-spring/src/main/resources/templates/menu.mustache b/kotlin-spring/src/main/resources/templates/menu.mustache new file mode 100644 index 0000000..c4d9ba0 --- /dev/null +++ b/kotlin-spring/src/main/resources/templates/menu.mustache @@ -0,0 +1,23 @@ + diff --git a/kotlin-spring/src/test/resources/junit-platform.properties b/kotlin-spring/src/test/resources/junit-platform.properties new file mode 100644 index 0000000..ab79697 --- /dev/null +++ b/kotlin-spring/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.testinstance.lifecycle.default = per_class diff --git a/wicket/.github/workflows/gradle.yml b/wicket/.github/workflows/gradle.yml new file mode 100644 index 0000000..58e1c59 --- /dev/null +++ b/wicket/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/wicket/.gitignore b/wicket/.gitignore new file mode 100644 index 0000000..2e1f2c3 --- /dev/null +++ b/wicket/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +build/ +bin/ +.classpath +.project +.settings/ +.asciidoctorconfig.adoc diff --git a/wicket/.gitlab-ci.yml b/wicket/.gitlab-ci.yml new file mode 100644 index 0000000..514cefd --- /dev/null +++ b/wicket/.gitlab-ci.yml @@ -0,0 +1,36 @@ +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +before_script: + - source "/home/gitlab-runner/.sdkman/bin/sdkman-init.sh" + - sdk u java 11.0.12-open + +stages: + - build + - test + - analysis + - publish + +Build Application: + stage: build + script: ./gradlew build + +Create Documentation: + stage: build + script: ./gradlew asciidoctorPdf + +Test Application: + stage: test + script: ./gradlew check + +sonarqube-check: + stage: analysis + variables: + SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache + GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task + script: ./gradlew sonarqube + allow_failure: true + +Publish Artifacts: + stage: publish + script: ./gradlew publish diff --git a/wicket/README.md b/wicket/README.md new file mode 100644 index 0000000..ab12d07 --- /dev/null +++ b/wicket/README.md @@ -0,0 +1,4 @@ +![Java CI with Gradle](https://github.com/tpeetz/kontor-wicket/workflows/Java%20CI%20with%20Gradle/badge.svg) + +# kontor-wicket +Kontor Application with Apache Wicket diff --git a/wicket/build.gradle b/wicket/build.gradle new file mode 100644 index 0000000..0afbe9f --- /dev/null +++ b/wicket/build.gradle @@ -0,0 +1,61 @@ +plugins { + id 'war' + id 'jacoco-report-aggregation' + alias(versions.plugins.asciidoctorConvention) + alias(versions.plugins.javaConvention) + alias(versions.plugins.sonarqube) +} + +final BUILD_DATE = new Date().format('dd.MM.yyyy').toString() + +dependencies { + implementation versions.slf4j + implementation versions.commonscli + testImplementation versions.junit + implementation versions.bundles.logback + spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0' + + implementation 'org.apache.wicket:wicket:9.9.1' + implementation 'org.apache.wicket:wicket-extensions:9.9.1' + testImplementation 'org.eclipse.jetty:jetty-webapp:9.4.44.v20210927' + testImplementation 'org.eclipse.jetty:jetty-jmx:9.4.44.v20210927' +} + +publishing { + publications { + application(MavenPublication) { + groupId = group + from components.java + } + } +} + +jacocoTestReport { + reports { + xml.enabled true + } +} + +test.finalizedBy jacocoTestReport + +spotbugs { + ignoreFailures = true +} + +sonarqube { + properties { + property "sonar.projectKey", "kontor_kontor-wicket_AYCAQ47WRCd9N673sSSD" + property "sonar.host.url", "https://sonar.thpeetz.de" + property "sonar.login", "7cf604ab1ebf48f7dc60d942c8196a132b228a6d" + property "sonar.qualitygate.wait", true + property "sonar.sourceEncoding", "UTF-8" + } +} + +tasks.named('sonarqube').configure { + dependsOn test +} + +wrapper { + gradleVersion = "7.5" +} diff --git a/wicket/gradle.properties b/wicket/gradle.properties new file mode 100644 index 0000000..e140eca --- /dev/null +++ b/wicket/gradle.properties @@ -0,0 +1,3 @@ +description='Anwendung Kontor mit Apache Wicket' +group=de.thpeetz +version=1.0.0-SNAPSHOT diff --git a/wicket/gradle/wrapper/gradle-wrapper.jar b/wicket/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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/wicket/gradlew.bat b/wicket/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/wicket/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% 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/wicket/settings.gradle b/wicket/settings.gradle new file mode 100644 index 0000000..f66ecb4 --- /dev/null +++ b/wicket/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "kontor-wicket" diff --git a/wicket/src/docs/asciidoc/kontor-wicket.adoc b/wicket/src/docs/asciidoc/kontor-wicket.adoc new file mode 100644 index 0000000..e310b6f --- /dev/null +++ b/wicket/src/docs/asciidoc/kontor-wicket.adoc @@ -0,0 +1,179 @@ += Projektbeschreibung kontor-wicket: Entwicklungs- und Projekthandbuch +:author: Thomas Peetz +:email: +:doctype: book +:sectnums: +:sectnumlevels: 4 +:toc: +:toclevels: 4 +:table-caption!: +:counter: table-number: 0 + +[title="Dokumenthistorie", id="Table-{counter:table-number}", options="header"] +|=== +| Version | Datum | Autor | Änderungsgrund / Bemerkungen +| 1.0.0 | 16.05.2022 | Thomas Peetz | Ersterstellung +|=== + +== Allgemeines + +=== Zweck des Dokumentes + +Das Entwicklungshandbuch beschreibt die Werkzeuge und die Vorgehensweise bei der Entwicklung +im Projekt kontor-wicket und der Erstellung der Dokumentation. + +=== Verwendete Tools + +==== Gitlab + +Für die Verwaltung des Sourcecode kommt ((Gitlab))<> zum Einsatz. +Mit Gitlab werden auch die Projektaufgaben verwaltet. + +Das Projekt und das dazugehörige Git Repository sind unter der Adresse + +https://gitlab.ingenieurbuero-peetz.de/kontor/kontor-wicket + +zu finden. + +== Erstellung der Dokumentation + +Die Dokumentation des Projektes wird mit ((Asciidoctor))<> geschrieben. +Die Dokumente erhalten ihre Namen nach dem jeweiligen Hauptdokument. + +=== Quellcode Verwaltung + +Die Asciidoctor-Dateien haben die Endung `.adoc`. + +=== Buildsystem + +Zur Erstellung der PDF-Dateien aus den Asciidoctor-Dateien wird das Buildsystem ((Gradle))<<3>> verwendet. +Die Dateien für die Dokumente liegen im Verzeichnis `src/docs/asciidoc`. + +Der Gradle Build wird über die Datei `build.gradle` definiert. + + +== Einführung + +=== Zweck + +=== Stakeholder des Systems + +=== Systemumfang + +==== Zielsetzung des Systems + +=== Systemübersicht + +==== Systemkontext + +==== Systemarchitektur + +==== Systemschnittstellen + +===== Realisierte Schnittstellen + +===== Verwendete Schnittstellen + +==== Logisches Datenmodell + +==== Einschränkungen + +== Anforderungen der Domäne + +=== Systemfunktionen + +==== Anwendungsfälle + +==== Akteure + +==== Zielgruppen + +=== Anforderungen + +==== Anforderungen an externe Schnittstellen + +==== Funktionale Anforderungen + +==== Qualitätsanforderungen + +==== Randbedingungen + +==== Weitere Anforderungen + +==== Wartungs- und Supportinformationen + +=== Verifikation + +== Projektbeschreibung + +=== Ausgangslage + +//==== Rechtliche Vorgaben und Rahmenbedingungen +//=== Rahmenbedingungen + +//==== Vorhandene Regelungen + +=== Projektziele + +=== Projektabgrenzung + +//=== Voraussichtliche Kosten + +//=== Projektrisiken + +//==== Produktivität + +//==== Finanzielle Risiken + +//==== Akzeptanz + +== Projektorganisation + +=== Projekt-Aufbauorganisation + +=== Rollendefinition + +//==== Projektauftraggeber + +//==== Projektausschuss + +//==== Beratung / Qualitätssicherung + +==== Projekteiter + +==== Projektteam + +==== Liste der Stakeholder + +=== Projektablauforganisation + +==== Projekt-Phasen + +===== Erstellung der Projektdokumentation + + +== Verschiedenes + +=== Erreichbarkeiten + +[bibliography] +== Referenzen + +- [[[asciidoctor]]] http://asciidoctor.org +- [[[gitlab]]] http://www.gitlab.org +- [[[gradle]]] http://www.gradle.org +- [[[jenkins]]] http://jenkins-ci.org + +[glossary] +== Glossar + +[index] +== Index + +== Verzeichnisse + +=== Abbildungsverzeichnis + +=== Tabellenverzeichnis + +<> <> diff --git a/wicket/src/main/java/de/thpeetz/kontor/HomePage.java b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java new file mode 100644 index 0000000..e5a3c82 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/HomePage.java @@ -0,0 +1,18 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.request.mapper.parameter.PageParameters; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.WebPage; + +public class HomePage extends WebPage { + private static final long serialVersionUID = 1L; + + public HomePage(final PageParameters parameters) { + super(parameters); + + add(new Label("version", getApplication().getFrameworkSettings().getVersion())); + + // TODO Add your page's components here + + } +} diff --git a/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java new file mode 100644 index 0000000..9c39858 --- /dev/null +++ b/wicket/src/main/java/de/thpeetz/kontor/KontorApplication.java @@ -0,0 +1,41 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.csp.CSPDirective; +import org.apache.wicket.csp.CSPDirectiveSrcValue; +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.protocol.http.WebApplication; + +/** + * Application object for your web application. + * If you want to run this application without deploying, run the Start class. + * + * @see de.thpeetz.kontor.Start#main(String[]) + */ +public class KontorApplication extends WebApplication +{ + /** + * @see org.apache.wicket.Application#getHomePage() + */ + @Override + public Class getHomePage() + { + return HomePage.class; + } + + /** + * @see org.apache.wicket.Application#init() + */ + @Override + public void init() + { + super.init(); + + // needed for the styling used by the quickstart + getCspSettings().blocking() + .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.SELF) + .add(CSPDirective.STYLE_SRC, "https://fonts.googleapis.com/css") + .add(CSPDirective.FONT_SRC, "https://fonts.gstatic.com"); + + // add your configuration here + } +} diff --git a/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html new file mode 100644 index 0000000..4ca32fc --- /dev/null +++ b/wicket/src/main/resources/de/thpeetz/kontor/HomePage.html @@ -0,0 +1,62 @@ + + + + + Apache Wicket Quickstart + + + + +

+ +
+
+

Congratulations!

+

+ Your quick start works! This project is especially useful to + start developing your Wicket application or to create a test + case for a bug report. +

+

Get started

+

+ You can even switch to HTTPS! +

+

+ From here you can start hacking away at your application and + wow your clients: +

+ +

Get help

+

+ We are here to help! +

+ +

Reporting a bug

+

+ Help us help you: +

+
    +
  1. reproduce the bug with the least amount of code
  2. +
  3. create a unit test that shows the bug
  4. +
  5. fix the bug and create a patch
  6. +
  7. attach the result of step 1, 2 or 3 to a JIRA issue
  8. +
  9. profit!
  10. +
+

+ Please mention the correct Wicket version: 1.5-SNAPSHOT. +

+
+
+
+ + diff --git a/wicket/src/main/webapp/WEB-INF/web.xml b/wicket/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f5b288e --- /dev/null +++ b/wicket/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,32 @@ + + + + kontor-wicket + + + + + wicket.kontor-wicket + org.apache.wicket.protocol.http.WicketFilter + + applicationClassName + de.thpeetz.kontor.KontorApplication + + + + + wicket.kontor-wicket + /* + + diff --git a/wicket/src/main/webapp/logo.png b/wicket/src/main/webapp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39ec54854b748ab6aeb6b3965d88f452772d7d8e GIT binary patch literal 12244 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 i1O^!^0gjqN1OFf9qq%!F8)!QK0000`K literal 0 HcmV?d00001 diff --git a/wicket/src/main/webapp/style.css b/wicket/src/main/webapp/style.css new file mode 100644 index 0000000..87576a7 --- /dev/null +++ b/wicket/src/main/webapp/style.css @@ -0,0 +1,68 @@ +body, p, li, a { font-family: georgia, times, serif;font-size:13pt;} +h1, h2, h3 { font-family: 'Yanone Kaffeesatz', arial, serif; } +body { margin:0;padding:0;} +#hd { + width : 100%; + height : 87px; + background-color : #092E67; + margin-top : 0; + padding-top : 10px; + border-bottom : 1px solid #888; + z-index : 0; +} +#ft { + position : absolute; + bottom : 0; + width : 100%; + height : 99px; + background-color : #6493D2; + border-top : 1px solid #888; + z-index : 0; +} +#logo,#bd { + width : 650px; + margin: 0 auto; + padding: 25px 50px 0 50px; +} +#logo h1 { + color : white; + font-size:36pt; + display: inline; +} +#logo img { + display:inline; + vertical-align: bottom; + margin-left : 50px; + margin-right : 5px; +} +body { margin-top : 0; padding-top : 0;} +#logo, #logo h1 { margin-top : 0; padding-top : 0;} +#bd { + position : absolute; + top : 75px; + bottom : 75px; + left : 50%; + margin-left : -325px; + z-index : 1; + overflow: auto; + background-color : #fff; + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + -moz-box-shadow: 0px 0px 10px #888; + -webkit-box-shadow: 0px 0px 10px #888; + box-shadow: 0px 0px 10px #888; +} +a, a:visited, a:hover, a:active { + color : #6493D2; +} +h2 { + padding : 0; margin:0; + font-size:36pt; + color:#FF5500; +} +h3 { + padding : 0; margin:0; + font-size:24pt; + color:#092E67; +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/Start.java b/wicket/src/test/java/de/thpeetz/kontor/Start.java new file mode 100644 index 0000000..49c740a --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/Start.java @@ -0,0 +1,106 @@ +package de.thpeetz.kontor; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; + +import org.eclipse.jetty.jmx.MBeanContainer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.webapp.WebAppContext; + +/** + * Separate startup class for people that want to run the examples directly. Use parameter + * -Dcom.sun.management.jmxremote to startup JMX (and e.g. connect with jconsole). + */ +public class Start +{ + /** + * Main function, starts the jetty server. + * + * @param args + */ + public static void main(String[] args) + { + System.setProperty("wicket.configuration", "development"); + + Server server = new Server(); + + HttpConfiguration http_config = new HttpConfiguration(); + http_config.setSecureScheme("https"); + http_config.setSecurePort(8443); + http_config.setOutputBufferSize(32768); + + ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(http_config)); + http.setPort(8080); + http.setIdleTimeout(1000 * 60 * 60); + + server.addConnector(http); + + Resource keystore = Resource.newClassPathResource("/keystore.p12"); + if (keystore != null && keystore.exists()) + { + // if a keystore for a SSL certificate is available, start a SSL + // connector on port 8443. + // By default, the quickstart comes with a Apache Wicket Quickstart + // Certificate that expires about half way september 2031. Do not + // use this certificate anywhere important as the passwords are + // available in the source. + + SslContextFactory sslContextFactory = new SslContextFactory(); + sslContextFactory.setKeyStoreResource(keystore); + sslContextFactory.setKeyStorePassword("wicket"); + sslContextFactory.setKeyManagerPassword("wicket"); + + HttpConfiguration https_config = new HttpConfiguration(http_config); + https_config.addCustomizer(new SecureRequestCustomizer()); + + ServerConnector https = new ServerConnector(server, new SslConnectionFactory( + sslContextFactory, "http/1.1"), new HttpConnectionFactory(https_config)); + https.setPort(8443); + https.setIdleTimeout(500000); + + server.addConnector(https); + System.out.println("SSL access to the examples has been enabled on port 8443"); + System.out + .println("You can access the application using SSL on https://localhost:8443"); + System.out.println(); + } + + WebAppContext bb = new WebAppContext(); + bb.setServer(server); + bb.setContextPath("/"); + bb.setWar("src/main/webapp"); + + // uncomment the next two lines if you want to start Jetty with WebSocket (JSR-356) support + // you need org.apache.wicket:wicket-native-websocket-javax in the classpath! + // ServerContainer serverContainer = WebSocketServerContainerInitializer.configureContext(bb); + // serverContainer.addEndpoint(new WicketServerEndpointConfig()); + + // uncomment next line if you want to test with JSESSIONID encoded in the urls + // ((AbstractSessionManager) + // bb.getSessionHandler().getSessionManager()).setUsingCookies(false); + + server.setHandler(bb); + + MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer(); + MBeanContainer mBeanContainer = new MBeanContainer(mBeanServer); + server.addEventListener(mBeanContainer); + server.addBean(mBeanContainer); + + try + { + server.start(); + server.join(); + } catch (Exception e) { + e.printStackTrace(); + System.exit(100); + } + } +} diff --git a/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java new file mode 100644 index 0000000..fe9c409 --- /dev/null +++ b/wicket/src/test/java/de/thpeetz/kontor/TestHomePage.java @@ -0,0 +1,29 @@ +package de.thpeetz.kontor; + +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Simple test using the WicketTester + */ +public class TestHomePage +{ + private WicketTester tester; + + @BeforeEach + public void setUp() + { + tester = new WicketTester(new KontorApplication()); + } + + @Test + public void homepageRendersSuccessfully() + { + //start and render the test page + tester.startPage(HomePage.class); + + //assert rendered page class + tester.assertRenderedPage(HomePage.class); + } +} diff --git a/wicket/src/test/resources/jetty/jetty-http.xml b/wicket/src/test/resources/jetty/jetty-http.xml new file mode 100644 index 0000000..7b39acb --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-http.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty-https.xml b/wicket/src/test/resources/jetty/jetty-https.xml new file mode 100644 index 0000000..35100e7 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-https.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + http/1.1 + + + + + + + + + + + + + 30000 + + + + diff --git a/wicket/src/test/resources/jetty/jetty-ssl.xml b/wicket/src/test/resources/jetty/jetty-ssl.xml new file mode 100644 index 0000000..f23231b --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty-ssl.xml @@ -0,0 +1,57 @@ + + + + + + + + + / + + + + + + SSL_RSA_WITH_DES_CBC_SHA + SSL_DHE_RSA_WITH_DES_CBC_SHA + SSL_DHE_DSS_WITH_DES_CBC_SHA + SSL_RSA_EXPORT_WITH_RC4_40_MD5 + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + TLS_RSA_WITH_AES_256_GCM_SHA384 + TLS_RSA_WITH_AES_128_GCM_SHA256 + TLS_RSA_WITH_AES_256_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_256_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_RSA_WITH_AES_256_CBC_SHA + TLS_DHE_DSS_WITH_AES_256_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA256 + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_RSA_WITH_AES_128_CBC_SHA + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_RSA_WITH_AES_128_CBC_SHA + TLS_DHE_DSS_WITH_AES_128_CBC_SHA + + + + + + + + + + + + + + + diff --git a/wicket/src/test/resources/jetty/jetty.xml b/wicket/src/test/resources/jetty/jetty.xml new file mode 100644 index 0000000..5590715 --- /dev/null +++ b/wicket/src/test/resources/jetty/jetty.xml @@ -0,0 +1,23 @@ + + + + + + + + https + + + + 32768 + 8192 + 8192 + true + false + 512 + + + + + + diff --git a/wicket/src/test/resources/keystore b/wicket/src/test/resources/keystore new file mode 100644 index 0000000000000000000000000000000000000000..1473db33287b4de9be58dc79741ac9d5c088919a GIT binary patch literal 3954 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 e{K-i+ON5SRjUU_1uu~kDP_^p8r(#`g+`j Date: Wed, 30 Apr 2025 18:40:24 +0200 Subject: [PATCH 2/2] create release 0.1.0 --- kontor-api/Makefile | 2 +- kontor-spring/Dockerfile | 2 +- kontor-spring/gradle.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kontor-api/Makefile b/kontor-api/Makefile index 9c05f76..2cf2c7f 100644 --- a/kontor-api/Makefile +++ b/kontor-api/Makefile @@ -7,7 +7,7 @@ test: DB_HOST=localhost uv run pytest -v --cov --cov-report=term --cov-report=html:coverage-report docker: clean - docker build --target=production -t kontor-api -t kontor-api:0.1.0-SNAPSHOT . + docker build --target=production -t kontor-api -t kontor-api:0.1.0 -t kontor-api . dev: MARIADB_SERVER=localhost uv run fastapi dev src/main.py --port 8008 diff --git a/kontor-spring/Dockerfile b/kontor-spring/Dockerfile index d4f3463..d49d591 100644 --- a/kontor-spring/Dockerfile +++ b/kontor-spring/Dockerfile @@ -1,5 +1,5 @@ FROM alpine/java:21-jdk WORKDIR / -ADD build/libs/kontor-spring-0.1.0-SNAPSHOT.jar app.jar +ADD build/libs/kontor-spring-0.1.0.jar app.jar EXPOSE 8000 CMD ["java", "-jar", "-Dspring.profiles.active=prod", "-Dvaadin.productionMode=true", "app.jar"] diff --git a/kontor-spring/gradle.properties b/kontor-spring/gradle.properties index f305204..6187abe 100644 --- a/kontor-spring/gradle.properties +++ b/kontor-spring/gradle.properties @@ -1,3 +1,3 @@ description='Kontor with Spring Boot' -version=0.1.0-SNAPSHOT +version=0.1.0 group=de.thpeetz