├── .gitignore ├── LICENSE ├── README.md ├── descriptions ├── alextheregent-mod-installer-description.json ├── default-mod-installer-description.json ├── findarkside-mod-installer-description.json ├── lucasio6-mod-installer-description.json ├── mikeypdog-mod-installer-description.json ├── nbakulev-mod-installer-description.json ├── shieldheart-mod-installer-description.json ├── uberfox-mod-installer-description.json ├── wulfmarius-mod-installer-description.json ├── xpazeman-mod-installer-description.json └── zeobviouslyfakeacc-mod-installer-description.json ├── images ├── installation-directory.png ├── installer-01.png └── update-available.png ├── pom.xml ├── src ├── main │ ├── java │ │ └── me │ │ │ └── wulfmarius │ │ │ └── modinstaller │ │ │ ├── AbortException.java │ │ │ ├── Asset.java │ │ │ ├── DependencyResolver.java │ │ │ ├── InstallationsChangedListener.java │ │ │ ├── InstallationsChangedListeners.java │ │ │ ├── Listeners.java │ │ │ ├── MissingDependencyException.java │ │ │ ├── ModDefinition.java │ │ │ ├── ModDefinitions.java │ │ │ ├── ModDependencies.java │ │ │ ├── ModDependency.java │ │ │ ├── ModInstaller.java │ │ │ ├── ModInstallerException.java │ │ │ ├── ProgressListener.java │ │ │ ├── ProgressListeners.java │ │ │ ├── Resolution.java │ │ │ ├── SourcesChangedListener.java │ │ │ ├── SourcesChangedListeners.java │ │ │ ├── Version.java │ │ │ ├── VersionRequirement.java │ │ │ ├── compatibility │ │ │ ├── CompatibilityChecker.java │ │ │ ├── CompatibilityState.java │ │ │ ├── CompatibilityVersion.java │ │ │ └── CompatibilityVersions.java │ │ │ ├── repository │ │ │ ├── DependencyResolution.java │ │ │ ├── DownloadResponseExtractor.java │ │ │ ├── Installation.java │ │ │ ├── Installations.java │ │ │ ├── RateLimitException.java │ │ │ ├── Repository.java │ │ │ ├── RepositoryException.java │ │ │ ├── Source.java │ │ │ ├── SourceDescription.java │ │ │ ├── SourceException.java │ │ │ ├── SourceFactory.java │ │ │ ├── Sources.java │ │ │ └── source │ │ │ │ ├── AbstractSourceFactory.java │ │ │ │ ├── DirectSourceFactory.java │ │ │ │ ├── FileSourceFactory.java │ │ │ │ ├── GithubAsset.java │ │ │ │ ├── GithubAuthor.java │ │ │ │ ├── GithubRelease.java │ │ │ │ └── GithubSourceFactory.java │ │ │ ├── rest │ │ │ ├── HostUnreachableException.java │ │ │ ├── RateLimitException.java │ │ │ ├── RestClient.java │ │ │ └── RestClientException.java │ │ │ ├── ui │ │ │ ├── BindingsFactory.java │ │ │ ├── ChangeLogViewer.fxml │ │ │ ├── ChangeLogViewerController.java │ │ │ ├── ControllerFactory.java │ │ │ ├── InstallerMainPanel.fxml │ │ │ ├── InstallerMainPanelController.java │ │ │ ├── ModDetailsPanel.fxml │ │ │ ├── ModDetailsPanelController.java │ │ │ ├── ModInstallerEvent.java │ │ │ ├── ModInstallerMain.java │ │ │ ├── ModInstallerUI.java │ │ │ ├── ProgressDialog.fxml │ │ │ ├── ProgressDialogController.java │ │ │ ├── RefreshableObjectProperty.java │ │ │ └── WindowBecameVisibleHandler.java │ │ │ ├── update │ │ │ ├── UpdateChecker.java │ │ │ └── UpdateState.java │ │ │ └── utils │ │ │ ├── JsonUtils.java │ │ │ ├── OsUtils.java │ │ │ └── StringUtils.java │ └── resources │ │ ├── add_circle_outline_black_24x24.png │ │ ├── arrow_upward_black_18x18.png │ │ ├── baseline_access_time_red_18x18.png │ │ ├── baseline_access_time_red_24x24.png │ │ ├── baseline_filter_list_black_24x24.png │ │ ├── default-compatibility-state.json │ │ ├── default-sources.json │ │ ├── folder_open_black_24.png │ │ ├── global.css │ │ ├── icon.png │ │ ├── lock_grey_18x18.png │ │ ├── lock_grey_24x24.png │ │ ├── new_releases_grey_18x18.png │ │ └── refresh_black_24x24.png └── test │ ├── java │ └── me │ │ └── wulfmarius │ │ └── modinstaller │ │ ├── DependencyResolverTest.java │ │ └── VersionTest.java │ └── resources │ ├── mod-a.json │ ├── mod-b.json │ ├── mod-c.json │ ├── mod-d.json │ └── mod-e.json └── tld-versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | mod-installer/ 2 | mods/ 3 | target/ 4 | .classpath 5 | .project 6 | .settings 7 | dependency-reduced-pom.xml 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 WulfMarius 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mod-Installer 2 | 3 | 4 | -------------------------------------------------------------------------------- /descriptions/alextheregent-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AlexTheRegent Collection", 3 | "description": "Mods of AlexTheRegent", 4 | "url": "https://github.com/AlexTheRegent", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/AlexTheRegent/tld-GameTweaks" 8 | ] 9 | } -------------------------------------------------------------------------------- /descriptions/default-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Default Collection", 3 | "description": "Default Mod Collection", 4 | "url": "https://github.com/WulfMarius/Mod-Installer/tree/master/descriptions", 5 | "releases": [], 6 | "definitions": [ 7 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/alextheregent-mod-installer-description.json", 8 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/findarkside-mod-installer-description.json", 9 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/lucasio6-mod-installer-description.json", 10 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/mikeypdog-mod-installer-description.json", 11 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/nbakulev-mod-installer-description.json", 12 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/shieldheart-mod-installer-description.json", 13 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/uberfox-mod-installer-description.json", 14 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/wulfmarius-mod-installer-description.json", 15 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/xpazeman-mod-installer-description.json", 16 | "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/descriptions/zeobviouslyfakeacc-mod-installer-description.json" 17 | ] 18 | } -------------------------------------------------------------------------------- /descriptions/findarkside-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FINDarkside Collection", 3 | "description": "Mods of FINDarkside", 4 | "url": "https://github.com/FINDarkside", 5 | "author": "FINDarkside", 6 | "releases": [ 7 | { 8 | "name": "Developer-Console", 9 | "version": "1.1", 10 | "releaseDate": "2018-11-04", 11 | "description": "Enables the developer console, which can be opened by pressing F1\nType help for a list of commands and press TAB to autocomplete.", 12 | "url": "https://github.com/FINDarkside/TLD-Developer-Console/releases/tag/1.1", 13 | "changes": "- Scroll down when a command is submitted\n- Fixed save, currentSceneName (now called scene_name), and currentSceneIndex (now called scene_index\n- Added new pos command to get the current position\n- Added new tp command to teleport", 14 | "compatibleWith": "V1.41", 15 | "dependencies": [], 16 | "assets": [ 17 | { 18 | "url": "https://github.com/FINDarkside/TLD-Developer-Console/releases/download/1.1/DeveloperConsole.dll" 19 | } 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /descriptions/lucasio6-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lucasio6 Collection", 3 | "description": "Mods of Lucasio6", 4 | "url": "https://github.com/lucasio6", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/lucasio6/Flint", 8 | "https://github.com/lucasio6/Lint", 9 | "https://github.com/lucasio6/Wolfhat" 10 | ] 11 | } -------------------------------------------------------------------------------- /descriptions/mikeypdog-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MikeyPdog Collection", 3 | "description": "Mods of MikeyPdog", 4 | "url": "https://github.com/MikeyPdog", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/MikeyPdog/TLD-Minimods" 8 | ] 9 | } -------------------------------------------------------------------------------- /descriptions/nbakulev-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nbakulev Collection", 3 | "description": "Mods of nbakulev", 4 | "url": "https://github.com/nbakulev", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/nbakulev/Deerskincoat", 8 | "https://github.com/nbakulev/Woodenstatuettes" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /descriptions/shieldheart-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ShieldHeart Collection", 3 | "description": "Mods of ShieldHeart", 4 | "url": "http://www.moddb.com/members/shieldhearttld", 5 | "author": "ShieldHeart", 6 | "releases": [ 7 | { 8 | "name": "RelentlessNight", 9 | "version": "v3.00", 10 | "releaseDate": "2018-03-25", 11 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.", 12 | "url": "http://www.moddb.com/mods/relentlessnight/downloads/relentlessnight-v3-00", 13 | "changes": "- (new) All mod gameplay features can now be customized or turned off individually through the custom game setup menu.\n- (new) Indoor environments will retain heat from fires much longer after the fire has burnt out. The hotter the fire, the longer it will keep the place warm. In enclosed buildings, fires will heat the whole house/space and better insulated environments will keep warm for longer.\n- (new) Temperature will now affect wildlife populations. Roaming wildlife will be less abundant and harder to find in extreme cold temperatures, but also more abundant in relatively warm temperatures.\n- (new) You can now adjust how long fuels will burn in a fire or how long lantern fuel will last while in use. All can be adjusted to provide up to 3 times their regular burn times.\n- (new) Fixed bug in vanilla game where loot inside of containers in custom games would generate incorrectly, causing things [like this](). The correct amount of loot will now generate for the associated difficulty chosen in custom mode.\n- (new) Fixed bug in vanilla game that prevented custom mode option \"Wake Up When Freezing\" from working. This feature is now functional and enabled in all Relentless Night games.\n- Days and nights get increasingly longer eventually ending in the tidal locking of the Earth and survival in perpetual darkness.\n- Temperatures become increasingly warmer/colder with duration of days/nights.\n- Indoor temperatures now have a slight correlation with outdoor temperature.\n- Blizzards and stronger winds become more frequent as days/nights become longer and weather becomes more extreme.\n- Roaming wildlife become increasingly scarce as outdoor conditions become more extreme and harsh.\n- Cabin fever affliction is disabled due to the eventuality of long nights and extreme outdoor temperatures.\n- Indoor environments can become slightly lit during nights depending on weather and available moonlight outdoors. During the brightest phases of the moon, crafting/breaking of items indoors can become available again.\n- Surveying is now possible at night, provided the weather is clear and enough light is present.\n- For every additional 1C below a feels-like temperature of -40C, the rate at which the player suffers freezing damage is increased. This effect can accumulate up to a maximum of 5x the original rate at a -100C feels-like temperature and below, do bring a jacket.\n- The mod scenario can be played in all sandbox difficulties and can be set up in its own sandbox page. The mod does not prevent playing regular sandbox or other vanilla modes. Relentless night saves are kept completely separate from other save data and cannot be played by regular sandbox mode and vice versa.\n- Note: Journal statistics and days survived are still tracked in regular 24-hour days, as if the player is still keeping track with a watch.\n", 14 | "dependencies": [], 15 | "assets": [ 16 | { 17 | "url": "https://www.moddb.com/downloads/mirror/135609/114/216ad7337ebffbc465a517399d7515cd", 18 | "zipDirectory": "ModFiles", 19 | "type": "zip" 20 | } 21 | ] 22 | }, { 23 | "name": "RelentlessNight", 24 | "version": "v3.01", 25 | "releaseDate": "2018-06-19", 26 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.", 27 | "url": "https://www.moddb.com/mods/relentlessnight/downloads/relentless-night-v301-133", 28 | "changes": "- Updated all mod features to work with new Vigilant Flame update.\n- Coals can now be added to a fire without a waiting period (late-game balancing).\n- More fuel can be added to a fire even if the fire has reached its maximum duration, provided that the fuel can still increase the fire's current temperature. Although the duration of fire will not increase further, the heat bonus will still be applied. (late-game balancing)\n- The immediate area where an outdoor fire burned will also retain some heat a while longer after the fire's out, although of course the temperature bonus will drop and disappear much quicker compared to the heat from a fire inside an insulated building.\n- Fixed mod bug where failing to start a fire indoors could reset the residual heat bonus remaining inside back to zero in RN's heat retention feature.\n- Fixed mod bug where blizzard temperature drop didn't change temperature during blizzards in certain Relentless Night runs.\n- Fixed mod bug where the temperature of outdoors caves would sometimes get calculated incorrectly and were warmer than intended.\n- Fixed mod bug where custom mode option \"Day Length Multiplier\" would cause timing issues in RN runs if it was set above 1x. All values of this option can now be chosen for RN runs as well.\n- Should you need to correct something in the RN settings later into your game, you can now change specific setting values through new console commands. Loading the save you want to change and entering \"rn_help\" into the console will provide information on all RN commands needed to change each setting.", 29 | "dependencies": [], 30 | "assets": [ 31 | { 32 | "url": "https://www.moddb.com/downloads/mirror/138754/108/2616759aa426e06e45dc22e3a1bdee7f", 33 | "zipDirectory": "ModFiles", 34 | "type": "zip" 35 | } 36 | ] 37 | }, { 38 | "name": "RelentlessNight", 39 | "version": "v3.02", 40 | "releaseDate": "2019-01-06", 41 | "description": "Relentless Night is a gameplay mod for The Long Dark that introduces a new sandbox scenario where the spin of the Earth is slowing down. Slowly but surely, days and nights are getting longer over time, leading to a world with continuously changing temperatures, weather patterns, wildlife populations, and more.", 42 | "url": "https://github.com/Shield-Heart/RelentlessNight", 43 | "changes": "Bugfix update for v3.01 to work with the TLD v1.41 Redux update", 44 | "dependencies": [ 45 | { 46 | "name": "ModSettings", 47 | "version": "^v1.4" 48 | } 49 | ], 50 | "assets": [ 51 | { 52 | "url": "https://github.com/Shield-Heart/RelentlessNight/releases/download/v3.02/RelentlessNight.dll" 53 | } 54 | ] 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /descriptions/uberfox-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UberFoX Collection", 3 | "description": "Mods of UberFoX", 4 | "url": "http://ubersoft.org/", 5 | "author": "UberFoX", 6 | "releases": [ 7 | { 8 | "name": "UModTld", 9 | "version": "1.7", 10 | "releaseDate": "2017-12-12", 11 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.", 12 | "url": "http://ubersoft.org/umodtld/", 13 | "changes": "Updated UModTld to fully support the new features of the December update so it can spawn mooses and heal broken ribs etc etc.\nAlso added a new command \"/custom\" that gives you the code for your current custom game just so you never lose it!.\n\nAdded various more things to the Config file such as jump settings, ability to show time of day on screen, hotkeys to spawn wildlife etc etc and all kinds of good stuff.", 14 | "dependencies": [], 15 | "assets": [ 16 | { 17 | "url": "http://ubersoft.org/download/UModTld17.zip" 18 | } 19 | ] 20 | }, { 21 | "name": "UModTld", 22 | "version": "1.7a", 23 | "releaseDate": "2018-03-13", 24 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.", 25 | "url": "http://ubersoft.org/umodtld/", 26 | "changes": "Updated UModTld to newest Long Dark version and newest Mod Loader since it seems a lot of bullshit was changed.", 27 | "dependencies": [], 28 | "assets": [ 29 | { 30 | "url": "http://ubersoft.org/download/UModTld17a.zip" 31 | } 32 | ] 33 | }, { 34 | "name": "UModTld", 35 | "version": "1.7b", 36 | "releaseDate": "2018-06-25", 37 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.", 38 | "url": "http://ubersoft.org/umodtld/", 39 | "changes": "Updated version probably has nothing new hard to remember but fixed a bug in newer versions of TLD", 40 | "dependencies": [], 41 | "assets": [ 42 | { 43 | "url": "http://ubersoft.org/download/UModTld17b.zip" 44 | } 45 | ] 46 | }, { 47 | "name": "UModTld", 48 | "version": "1.8a", 49 | "releaseDate": "2018-10-08", 50 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.", 51 | "url": "http://ubersoft.org/umodtld/", 52 | "changes": "Just a quick update to UModTld this one allows other mod developers to add their own console commands to UModTld", 53 | "dependencies": [], 54 | "assets": [ 55 | { 56 | "url": "http://ubersoft.org/download/UModTld18a.zip" 57 | } 58 | ] 59 | }, { 60 | "name": "UModTld", 61 | "version": "1.9", 62 | "releaseDate": "2018-12-21", 63 | "description": "UMod TLD is the first mod for The Long Dark. Currently it has cheats and general gameplay improvements.", 64 | "url": "http://ubersoft.org/umodtld/", 65 | "changes": "Updated UModTld to work on newest version now that I'm back", 66 | "dependencies": [], 67 | "assets": [ 68 | { 69 | "url": "https://www.moddb.com/downloads/mirror/172406/115/c68a2d3c87d4393cff25becaec41eedb", 70 | "type": "zip" 71 | } 72 | ] 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /descriptions/wulfmarius-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Wulf Marius Collection", 3 | "description": "Mods of Wulf Marius", 4 | "url": "https://github.com/WulfMarius", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/WulfMarius/Ankle-Support", 8 | "https://github.com/WulfMarius/AssetLoader", 9 | "https://github.com/WulfMarius/Better-Fuel-Management", 10 | "https://github.com/WulfMarius/Better-Night-Sky", 11 | "https://github.com/WulfMarius/Better-Placing", 12 | "https://github.com/WulfMarius/Better-Stacking", 13 | "https://github.com/WulfMarius/Better-Water-Management", 14 | "https://github.com/WulfMarius/Binoculars", 15 | "https://github.com/WulfMarius/Clothing-Pack", 16 | "https://github.com/WulfMarius/Coordinates-Grabber", 17 | "https://github.com/WulfMarius/Disable-Cabin-Fever", 18 | "https://github.com/WulfMarius/Disable-Chromatic-Aberration", 19 | "https://github.com/WulfMarius/Durable-Whetstone", 20 | "https://github.com/WulfMarius/Food-Pack", 21 | "https://github.com/WulfMarius/Free-Look-In-Cars", 22 | "https://github.com/WulfMarius/Home-Improvement", 23 | "https://github.com/WulfMarius/Instrument-Pack", 24 | "https://github.com/WulfMarius/Lonely-Orca", 25 | "https://github.com/WulfMarius/Map-Maker-Tools", 26 | "https://github.com/WulfMarius/ModComponent", 27 | "https://github.com/WulfMarius/NAudio-Unity", 28 | "https://github.com/WulfMarius/Preselect-Struggle-Weapon", 29 | "https://github.com/WulfMarius/Remove-Campfire", 30 | "https://github.com/WulfMarius/Silent-Aurora", 31 | "https://github.com/WulfMarius/Show-Map-Location", 32 | "https://github.com/WulfMarius/Solstice", 33 | "https://github.com/WulfMarius/Sort-Fire-Starters", 34 | "https://github.com/WulfMarius/Toggle-HUD" 35 | ] 36 | } -------------------------------------------------------------------------------- /descriptions/xpazeman-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Xpazeman Collection", 3 | "description": "Mods from xpazeman", 4 | "url": "https://github.com/Xpazeman", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/Xpazeman/Mod-Installer-Description" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /descriptions/zeobviouslyfakeacc-mod-installer-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeobviouslyfakeacc Collection", 3 | "description": "Mods of zeobviouslyfakeacc", 4 | "url": "https://github.com/zeobviouslyfakeacc", 5 | "releases": [], 6 | "definitions": [ 7 | "https://github.com/zeobviouslyfakeacc/Mod-Installer-Description" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /images/installation-directory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/installation-directory.png -------------------------------------------------------------------------------- /images/installer-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/installer-01.png -------------------------------------------------------------------------------- /images/update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WulfMarius/Mod-Installer/c03947570718b196f2dfe9570d0e4e51603fb045/images/update-available.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | jar 6 | 7 | me.wulfmarius 8 | mod-installer 9 | 0.7.3 10 | Mod-Installer 11 | 2018 12 | 13 | 14 | 15 | MIT License 16 | http://www.opensource.org/licenses/mit-license.php 17 | 18 | 19 | 20 | 21 | UTF-8 22 | 1.8 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-compiler-plugin 31 | 3.1 32 | 33 | ${project.build.compiler.version} 34 | ${project.build.compiler.version} 35 | ${project.build.sourceEncoding} 36 | 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-shade-plugin 41 | 3.1.1 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-shade-plugin 50 | 51 | 52 | 53 | 54 | sandbox 55 | 8.0 56 | me.wulfmarius.modinstaller.ui.ModInstallerUI 57 | WulfMarius 58 | ${project.version} 59 | 60 | 61 | 62 | 63 | 64 | 65 | package 66 | 67 | shade 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | src/main/java 77 | 78 | **/*.fxml 79 | 80 | 81 | 82 | src/main/resources 83 | 84 | * 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | com.fasterxml.jackson.core 93 | jackson-databind 94 | 2.9.8 95 | 96 | 97 | org.springframework 98 | spring-web 99 | 5.0.5.RELEASE 100 | 101 | 102 | junit 103 | junit 104 | 4.12 105 | test 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/AbortException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class AbortException extends ModInstallerException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public AbortException(String message) { 8 | super(message); 9 | } 10 | 11 | public AbortException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/Asset.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class Asset { 4 | 5 | private String url; 6 | private String targetDirectory; 7 | private String zipDirectory; 8 | private String type; 9 | 10 | public static Asset withUrl(String url) { 11 | Asset result = new Asset(); 12 | result.setUrl(url); 13 | return result; 14 | } 15 | 16 | public String getTargetDirectory() { 17 | return this.targetDirectory; 18 | } 19 | 20 | public String getType() { 21 | return this.type; 22 | } 23 | 24 | public String getUrl() { 25 | return this.url; 26 | } 27 | 28 | public String getZipDirectory() { 29 | return this.zipDirectory; 30 | } 31 | 32 | public void setTargetDirectory(String targetDirectory) { 33 | this.targetDirectory = targetDirectory; 34 | } 35 | 36 | public void setType(String type) { 37 | this.type = type; 38 | } 39 | 40 | public void setUrl(String url) { 41 | this.url = url; 42 | } 43 | 44 | public void setZipDirectory(String zipDirectory) { 45 | this.zipDirectory = zipDirectory; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/DependencyResolver.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | 6 | import me.wulfmarius.modinstaller.repository.*; 7 | 8 | public class DependencyResolver { 9 | 10 | private final Repository repository; 11 | private final Installations installations; 12 | 13 | private final ModDefinitions installed = new ModDefinitions(); 14 | private final Resolution resolution = new Resolution(); 15 | 16 | public DependencyResolver(Repository repository, Installations installations) { 17 | super(); 18 | 19 | this.repository = repository; 20 | this.installations = installations; 21 | } 22 | 23 | public Installations getInstallations() { 24 | return this.installations; 25 | } 26 | 27 | public Repository getRepository() { 28 | return this.repository; 29 | } 30 | 31 | public Resolution resolve(ModDefinition modDefinition) { 32 | this.initializeInstalled(); 33 | this.resolveDependencies(modDefinition); 34 | this.consolidate(); 35 | 36 | return this.resolution; 37 | } 38 | 39 | private void consolidate() { 40 | for (Iterator iterator = this.resolution.getInstall().iterator(); iterator.hasNext();) { 41 | ModDefinition eachInstall = iterator.next(); 42 | 43 | if (this.resolution.getUninstall().contains(eachInstall)) { 44 | iterator.remove(); 45 | this.resolution.getUninstall().remove(eachInstall); 46 | continue; 47 | } 48 | 49 | if (this.installations.contains(eachInstall)) { 50 | iterator.remove(); 51 | } 52 | } 53 | 54 | this.resolution.getInstall().reverse(); 55 | } 56 | 57 | private DependencyResolution findMatchingVersion(ModDependencies dependencies) { 58 | DependencyResolution result = new DependencyResolution(); 59 | result.setRequested(dependencies); 60 | 61 | Map matchingDependencies = new LinkedHashMap(); 62 | 63 | for (ModDependency eachDependency : dependencies) { 64 | matchingDependencies.put(eachDependency, this.repository.getMatching(eachDependency)); 65 | } 66 | 67 | result.setAvailable(matchingDependencies.values().stream().flatMap(ModDefinitions::stream).collect(Collectors.toSet())); 68 | 69 | ModDefinitions candidates = matchingDependencies.values().stream().reduce(null, ModDefinitions::intersect); 70 | candidates.getMin(ModDefinition::latest).ifPresent(result::setBestMatch); 71 | 72 | return result; 73 | } 74 | 75 | private void initializeInstalled() { 76 | this.installations.stream() 77 | .map(installation -> this.repository.getModDefinition(installation.getName(), installation.getVersion())) 78 | .filter(Optional::isPresent) 79 | .map(Optional::get) 80 | .forEach(this.installed::addModDefinition); 81 | } 82 | 83 | private void install(ModDefinition bestMatch) { 84 | this.resolution.addInstall(bestMatch); 85 | this.installed.addModDefinition(bestMatch); 86 | } 87 | 88 | private void resolveDependencies(ModDefinition modDefinition) { 89 | ModDefinition next = modDefinition; 90 | 91 | while (next != null) { 92 | this.uninstall(next); 93 | this.install(next); 94 | 95 | DependencyResolution dependencyResolution = this.installed.getAllDependencies() 96 | .values() 97 | .stream() 98 | .filter(dependencies -> !this.installed.satisfiesAll(dependencies)) 99 | .map(this::findMatchingVersion) 100 | .findFirst() 101 | .orElseGet(DependencyResolution::empty); 102 | 103 | if (!dependencyResolution.isAvailable()) { 104 | this.resolution.setMissingDependencies(dependencyResolution.getRequested()); 105 | } else if (!dependencyResolution.isResolved()) { 106 | this.resolution.setUnresolvableDependencies(dependencyResolution.getRequested()); 107 | } 108 | 109 | next = dependencyResolution.getBestMatch(); 110 | } 111 | } 112 | 113 | private void uninstall(ModDefinition modDefinition) { 114 | ModDefinitions installedVersions = this.installed.getModDefinitions(modDefinition.getName()); 115 | for (ModDefinition eachInstalledVersion : installedVersions) { 116 | this.installed.remove(eachInstalledVersion); 117 | } 118 | 119 | this.resolution.addUninstalls(this.installations.getInstallations(modDefinition.getName())); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/InstallationsChangedListener.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | @FunctionalInterface 4 | public interface InstallationsChangedListener { 5 | 6 | void changed(); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/InstallationsChangedListeners.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class InstallationsChangedListeners extends Listeners { 4 | 5 | public void changed() { 6 | this.fire(listener -> listener.changed()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/Listeners.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.*; 4 | import java.util.function.Consumer; 5 | 6 | public class Listeners { 7 | 8 | private final Set listeners = new HashSet<>(); 9 | 10 | public void addListener(T listener) { 11 | this.listeners.add(listener); 12 | } 13 | 14 | public void fire(Consumer action) { 15 | this.listeners.forEach(action); 16 | } 17 | 18 | public void removeListener(T listener) { 19 | this.listeners.remove(listener); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/MissingDependencyException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class MissingDependencyException extends ModInstallerException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | private final ModDependencies dependencies; 8 | 9 | public MissingDependencyException(String message, ModDependencies dependencies) { 10 | super(message); 11 | this.dependencies = dependencies; 12 | } 13 | 14 | public ModDependencies getDependencies() { 15 | return this.dependencies; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ModDefinition.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.*; 4 | import java.util.stream.Stream; 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | 8 | public class ModDefinition { 9 | 10 | private String sourceDefinition; 11 | 12 | private String name; 13 | private String url; 14 | private String author; 15 | private String version; 16 | private String description; 17 | private String changes; 18 | private Date releaseDate; 19 | private Date lastUpdated; 20 | private Asset[] assets; 21 | private ModDependency[] dependencies; 22 | private String compatibleWith; 23 | 24 | @JsonIgnore 25 | private transient Version parsedVersion; 26 | 27 | @JsonIgnore 28 | private transient Version parsedCompatibleWith; 29 | 30 | public static int latest(ModDefinition m1, ModDefinition m2) { 31 | return -Version.compare(m1.getParsedVersion(), m2.getParsedVersion()); 32 | } 33 | 34 | public boolean dependsOn(String modName) { 35 | return this.getDependenciesStream().anyMatch(dependency -> dependency.getName().equals(modName)); 36 | } 37 | 38 | @Override 39 | public boolean equals(Object obj) { 40 | if (this == obj) { 41 | return true; 42 | } 43 | 44 | if (!(obj instanceof ModDefinition)) { 45 | return false; 46 | } 47 | 48 | ModDefinition other = (ModDefinition) obj; 49 | if (!this.name.equals(other.name)) { 50 | return false; 51 | } 52 | 53 | if (this.getParsedVersion().compareTo(other.getParsedVersion()) != 0) { 54 | return false; 55 | } 56 | 57 | return true; 58 | } 59 | 60 | public Asset[] getAssets() { 61 | return this.assets; 62 | } 63 | 64 | public String getAuthor() { 65 | return this.author; 66 | } 67 | 68 | public String getChanges() { 69 | return this.changes; 70 | } 71 | 72 | public String getCompatibleWith() { 73 | return this.compatibleWith; 74 | } 75 | 76 | public ModDependency[] getDependencies() { 77 | return this.dependencies; 78 | } 79 | 80 | public Stream getDependenciesStream() { 81 | if (this.dependencies == null) { 82 | return Stream.empty(); 83 | } 84 | 85 | return Arrays.stream(this.dependencies); 86 | } 87 | 88 | public String getDescription() { 89 | return this.description; 90 | } 91 | 92 | public String getDisplayName() { 93 | return this.name + " " + this.version; 94 | } 95 | 96 | public Date getLastUpdated() { 97 | return this.lastUpdated; 98 | } 99 | 100 | public String getName() { 101 | return this.name; 102 | } 103 | 104 | public Version getParsedCompatibleWith() { 105 | if (this.parsedCompatibleWith == null && this.compatibleWith != null) { 106 | this.parsedCompatibleWith = Version.parse(this.compatibleWith); 107 | } 108 | 109 | return this.parsedCompatibleWith; 110 | } 111 | 112 | public Date getReleaseDate() { 113 | return this.releaseDate; 114 | } 115 | 116 | public String getSourceDefinition() { 117 | return this.sourceDefinition; 118 | } 119 | 120 | public String getUrl() { 121 | return this.url; 122 | } 123 | 124 | public String getVersion() { 125 | return this.version; 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | final int prime = 31; 131 | int result = 1; 132 | result = prime * result + (this.name == null ? 0 : this.name.hashCode()); 133 | result = prime * result + (this.version == null ? 0 : this.version.hashCode()); 134 | return result; 135 | } 136 | 137 | public boolean satisfies(ModDependency dependency) { 138 | if (!this.name.equals(dependency.getName())) { 139 | return false; 140 | } 141 | 142 | return dependency.isSatisfiedBy(this.getParsedVersion()); 143 | } 144 | 145 | public void setAssets(Asset[] assets) { 146 | this.assets = assets; 147 | } 148 | 149 | public void setAuthor(String author) { 150 | this.author = author; 151 | } 152 | 153 | public void setChanges(String changes) { 154 | this.changes = changes; 155 | } 156 | 157 | public void setCompatibleWith(String compatibleWith) { 158 | this.compatibleWith = compatibleWith; 159 | } 160 | 161 | public void setDependencies(ModDependency[] dependencies) { 162 | this.dependencies = dependencies; 163 | } 164 | 165 | public void setDescription(String description) { 166 | this.description = description; 167 | } 168 | 169 | public void setLastUpdated(Date lastUpdated) { 170 | this.lastUpdated = lastUpdated; 171 | } 172 | 173 | public void setName(String name) { 174 | this.name = name; 175 | } 176 | 177 | public void setReleaseDate(Date releaseDate) { 178 | this.releaseDate = releaseDate; 179 | } 180 | 181 | public void setSourceDefinition(String sourceDefinition) { 182 | this.sourceDefinition = sourceDefinition; 183 | } 184 | 185 | public void setUrl(String url) { 186 | this.url = url; 187 | } 188 | 189 | public void setVersion(String version) { 190 | this.version = version; 191 | } 192 | 193 | @Override 194 | public String toString() { 195 | return "(Definition " + this.name + ", " + this.version + ")"; 196 | } 197 | 198 | private Version getParsedVersion() { 199 | if (this.parsedVersion == null) { 200 | this.parsedVersion = Version.parse(this.version); 201 | } 202 | 203 | return this.parsedVersion; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ModDefinitions.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.*; 4 | import java.util.stream.*; 5 | 6 | import com.fasterxml.jackson.annotation.*; 7 | 8 | public class ModDefinitions implements Iterable { 9 | 10 | @JsonValue 11 | private List modDefinitions = new ArrayList<>(); 12 | 13 | @JsonCreator 14 | public static ModDefinitions create(ModDefinition... modDefinition) { 15 | ModDefinitions result = new ModDefinitions(); 16 | 17 | if (modDefinition != null) { 18 | for (ModDefinition eachModDefinition : modDefinition) { 19 | result.addModDefinition(eachModDefinition); 20 | } 21 | } 22 | 23 | return result; 24 | } 25 | 26 | public static ModDefinitions intersect(ModDefinitions definitions1, ModDefinitions definitions2) { 27 | if (definitions1 == null) { 28 | return definitions2; 29 | } 30 | 31 | if (definitions2 == null) { 32 | return definitions1; 33 | } 34 | 35 | ModDefinitions result = new ModDefinitions(); 36 | 37 | for (ModDefinition eachDefinition : definitions1) { 38 | if (definitions2.contains(eachDefinition)) { 39 | result.addModDefinition(eachDefinition); 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | 46 | public static ModDefinitions merge(ModDefinitions definitions1, ModDefinitions definitions2) { 47 | ModDefinitions result = new ModDefinitions(); 48 | 49 | result.addModDefinitions(definitions1); 50 | result.addModDefinitions(definitions2); 51 | 52 | return result; 53 | } 54 | 55 | public static Collector toModDefinitions() { 56 | return Collectors.reducing(new ModDefinitions(), ModDefinitions::create, ModDefinitions::merge); 57 | } 58 | 59 | public void addModDefinition(ModDefinition definition) { 60 | if (!this.modDefinitions.contains(definition)) { 61 | this.modDefinitions.add(definition); 62 | } 63 | } 64 | 65 | public void addModDefinitions(Iterable definitions) { 66 | definitions.forEach(this::addModDefinition); 67 | } 68 | 69 | public boolean contains(ModDefinition definition) { 70 | return this.modDefinitions.contains(definition); 71 | } 72 | 73 | public Map getAllDependencies() { 74 | return this.modDefinitions.stream().flatMap(ModDefinition::getDependenciesStream).collect( 75 | Collectors.toMap(ModDependency::getName, ModDependencies::create, ModDependencies::merge)); 76 | } 77 | 78 | public ModDefinitions getMatchingDefinitions(ModDependency dependency) { 79 | return this.modDefinitions.stream().filter(definition -> definition.satisfies(dependency)).collect(toModDefinitions()); 80 | } 81 | 82 | public Optional getMin(Comparator comparator) { 83 | return this.modDefinitions.stream().min(comparator); 84 | } 85 | 86 | public Optional getModDefinition(String name, String version) { 87 | return this.modDefinitions.stream() 88 | .filter(definition -> definition.getName().equals(name) && definition.getVersion().equals(version)) 89 | .findFirst(); 90 | } 91 | 92 | public ModDefinitions getModDefinitions(String name) { 93 | return this.modDefinitions.stream().filter(modDefinition -> modDefinition.getName().equals(name)).collect(toModDefinitions()); 94 | } 95 | 96 | public int getSize() { 97 | if (this.modDefinitions.isEmpty()) { 98 | return 0; 99 | } 100 | 101 | return this.modDefinitions.size(); 102 | } 103 | 104 | public boolean isEmpty() { 105 | return this.modDefinitions == null || this.modDefinitions.isEmpty(); 106 | } 107 | 108 | @Override 109 | public Iterator iterator() { 110 | return this.modDefinitions.iterator(); 111 | } 112 | 113 | public void remove(ModDefinition modDefinition) { 114 | this.modDefinitions.remove(modDefinition); 115 | } 116 | 117 | public void remove(String name) { 118 | if (name == null) { 119 | return; 120 | } 121 | 122 | this.modDefinitions.removeIf(modDefinition -> name.equals(modDefinition.getName())); 123 | } 124 | 125 | public void reverse() { 126 | Collections.reverse(this.modDefinitions); 127 | } 128 | 129 | public boolean satisfies(ModDependency modDependency) { 130 | return this.modDefinitions.stream().anyMatch(modDefinition -> modDefinition.satisfies(modDependency)); 131 | } 132 | 133 | public boolean satisfiesAll(ModDependencies modDependencies) { 134 | return modDependencies.stream().allMatch(this::satisfies); 135 | } 136 | 137 | public Stream stream() { 138 | return this.modDefinitions.stream(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ModDependencies.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.*; 4 | import java.util.stream.Stream; 5 | 6 | public class ModDependencies implements Iterable { 7 | 8 | private Set dependencies = new LinkedHashSet<>(); 9 | 10 | public static ModDependencies create(ModDependency dependency) { 11 | ModDependencies result = new ModDependencies(); 12 | result.dependencies.add(dependency); 13 | return result; 14 | } 15 | 16 | public static ModDependencies merge(ModDependencies dependencies1, ModDependencies dependencies2) { 17 | ModDependencies result = new ModDependencies(); 18 | result.addAll(dependencies1); 19 | result.addAll(dependencies2); 20 | return result; 21 | } 22 | 23 | public void add(ModDependency modDependency) { 24 | this.dependencies.add(modDependency); 25 | } 26 | 27 | public void addAll(Iterable additionalDependencies) { 28 | additionalDependencies.forEach(this::add); 29 | } 30 | 31 | public boolean contains(ModDependency modDependency) { 32 | return this.dependencies.contains(modDependency); 33 | } 34 | 35 | public int getSize() { 36 | if (this.isEmpty()) { 37 | return 0; 38 | } 39 | 40 | return this.dependencies.size(); 41 | } 42 | 43 | public boolean isEmpty() { 44 | return this.dependencies == null || this.dependencies.isEmpty(); 45 | } 46 | 47 | @Override 48 | public Iterator iterator() { 49 | if (this.dependencies == null) { 50 | return Collections.emptyIterator(); 51 | } 52 | 53 | return this.dependencies.iterator(); 54 | } 55 | 56 | public Stream stream() { 57 | if (this.dependencies == null) { 58 | return Stream.empty(); 59 | } 60 | 61 | return this.dependencies.stream(); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return this.dependencies.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ModDependency.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | 5 | public class ModDependency { 6 | 7 | private String name; 8 | private String version; 9 | 10 | @JsonIgnore 11 | private transient VersionRequirement versionRequirement; 12 | 13 | @Override 14 | public boolean equals(Object obj) { 15 | if (this == obj) { 16 | return true; 17 | } 18 | 19 | if (!(obj instanceof ModDependency)) { 20 | return false; 21 | } 22 | 23 | ModDependency other = (ModDependency) obj; 24 | 25 | if (!this.name.equals(other.name)) { 26 | return false; 27 | } 28 | 29 | if (!this.version.equals(other.version)) { 30 | return false; 31 | } 32 | 33 | return true; 34 | } 35 | 36 | public String getDisplayName() { 37 | return this.name + " " + this.version; 38 | } 39 | 40 | public String getName() { 41 | return this.name; 42 | } 43 | 44 | public String getVersion() { 45 | return this.version; 46 | } 47 | 48 | public VersionRequirement getVersionRequirement() { 49 | if (this.versionRequirement == null) { 50 | this.versionRequirement = VersionRequirement.parse(this.version); 51 | } 52 | 53 | return this.versionRequirement; 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | final int prime = 31; 59 | int result = 1; 60 | result = prime * result + (this.name == null ? 0 : this.name.hashCode()); 61 | result = prime * result + (this.version == null ? 0 : this.version.hashCode()); 62 | return result; 63 | } 64 | 65 | public boolean isSatisfiedBy(Version actualVersion) { 66 | return this.getVersionRequirement().isSatisfiedBy(actualVersion); 67 | } 68 | 69 | public void setName(String name) { 70 | this.name = name; 71 | } 72 | 73 | public void setVersion(String version) { 74 | this.version = version; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return "(Dependency: " + this.name + ", " + this.version + ")"; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ModInstallerException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class ModInstallerException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public ModInstallerException(String message) { 8 | super(message); 9 | } 10 | 11 | public ModInstallerException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ProgressListener.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public interface ProgressListener { 4 | 5 | void finished(String message); 6 | 7 | void started(String name); 8 | 9 | void stepDetail(String detail); 10 | 11 | void stepError(String error); 12 | 13 | void stepProgress(int completed, int total); 14 | 15 | void stepStarted(String step, StepType stepType); 16 | 17 | enum StepType { 18 | DOWNLOAD, INSTALL, UNINSTALL, REFRESH, ADD, INITIALIZE; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ProgressListeners.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import me.wulfmarius.modinstaller.ProgressListener.StepType; 4 | 5 | public class ProgressListeners extends Listeners { 6 | 7 | public void detail(String detail) { 8 | this.fire(listener -> listener.stepDetail(detail)); 9 | } 10 | 11 | public void error(String error) { 12 | this.fire(listener -> listener.stepError(error)); 13 | } 14 | 15 | public void finished() { 16 | this.finished(null); 17 | } 18 | 19 | public void finished(String message) { 20 | this.fire(listener -> listener.finished(message)); 21 | } 22 | 23 | public void started(String name) { 24 | this.fire(listener -> listener.started(name)); 25 | } 26 | 27 | public void stepProgress(int completed, int total) { 28 | this.fire(listener -> listener.stepProgress(completed, total)); 29 | } 30 | 31 | public void stepStarted(String step, StepType stepType) { 32 | this.fire(listener -> listener.stepStarted(step, stepType)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/Resolution.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import me.wulfmarius.modinstaller.repository.*; 4 | 5 | public class Resolution { 6 | 7 | private ModDefinitions install = new ModDefinitions(); 8 | private Installations uninstall = new Installations(); 9 | 10 | private ModDependencies missingDependencies; 11 | private ModDependencies unresolvableDependencies; 12 | 13 | public void addInstall(ModDefinition modDefinition) { 14 | this.install.addModDefinition(modDefinition); 15 | } 16 | 17 | public void addUninstalls(Iterable uninstalls) { 18 | this.uninstall.addInstallations(uninstalls); 19 | } 20 | 21 | public boolean containsUninstall(Installation installation) { 22 | return this.uninstall.contains(installation); 23 | } 24 | 25 | public ModDefinitions getInstall() { 26 | return this.install; 27 | } 28 | 29 | public ModDependencies getMissingDependencies() { 30 | return this.missingDependencies; 31 | } 32 | 33 | public Installations getUninstall() { 34 | return this.uninstall; 35 | } 36 | 37 | public ModDependencies getUnresolvableDependencies() { 38 | return this.unresolvableDependencies; 39 | } 40 | 41 | public boolean hasMissingDependencies() { 42 | return this.missingDependencies != null && !this.missingDependencies.isEmpty(); 43 | } 44 | 45 | public boolean hasUnresolvableDependencies() { 46 | return this.unresolvableDependencies != null && !this.unresolvableDependencies.isEmpty(); 47 | } 48 | 49 | public boolean isEmpty() { 50 | return this.install.isEmpty(); 51 | } 52 | 53 | public boolean isErroneous() { 54 | return this.hasMissingDependencies() || this.hasUnresolvableDependencies(); 55 | } 56 | 57 | public void setInstall(ModDefinitions install) { 58 | this.install = install; 59 | } 60 | 61 | public void setMissingDependencies(ModDependencies missingDependencies) { 62 | this.missingDependencies = missingDependencies; 63 | } 64 | 65 | public void setUninstall(Installations uninstall) { 66 | this.uninstall = uninstall; 67 | } 68 | 69 | public void setUnresolvableDependencies(ModDependencies unresolvableDependencies) { 70 | this.unresolvableDependencies = unresolvableDependencies; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/SourcesChangedListener.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | @FunctionalInterface 4 | public interface SourcesChangedListener { 5 | 6 | void changed(); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/SourcesChangedListeners.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | public class SourcesChangedListeners extends Listeners { 4 | 5 | public void changed() { 6 | this.fire(listener -> listener.changed()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/Version.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import static me.wulfmarius.modinstaller.utils.StringUtils.trimToEmpty; 4 | 5 | import java.util.Comparator; 6 | import java.util.regex.*; 7 | 8 | import org.springframework.util.StringUtils; 9 | 10 | public class Version implements Comparable { 11 | 12 | public static final String VERSION_UNKNOWN = "UNKNOWN"; 13 | 14 | // an extension to the semver.org pattern to accomodate UModTld 15 | private static final Pattern VERSION_PATTERN = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?(\\w+)?(?:-(\\w+))?"); 16 | 17 | public static final Comparator COMPARATOR = Comparator.comparingInt(Version::getMajor) 18 | .thenComparingInt(Version::getMinor) 19 | .thenComparingInt(Version::getPatch) 20 | .thenComparing(Version::getSpecial, Version::compareSpecial) 21 | .thenComparing(Version::getPrelease, Version::comparePrerelease); 22 | 23 | private int major; 24 | private int minor; 25 | private int patch; 26 | private String prelease; 27 | private String special; 28 | 29 | public static int compare(Version v1, Version v2) { 30 | return COMPARATOR.compare(v1, v2); 31 | } 32 | 33 | public static Version parse(String version) { 34 | if (version.startsWith("v") || version.startsWith("V")) { 35 | return parse(version.substring(1)); 36 | } 37 | 38 | Matcher matcher = VERSION_PATTERN.matcher(version); 39 | if (matcher.matches()) { 40 | Version result = new Version(); 41 | result.major = Integer.parseInt(matcher.group(1)); 42 | 43 | if (!StringUtils.isEmpty(matcher.group(2))) { 44 | result.minor = Integer.parseInt(matcher.group(2)); 45 | } 46 | 47 | if (!StringUtils.isEmpty(matcher.group(3))) { 48 | result.patch = Integer.parseInt(matcher.group(3)); 49 | } 50 | 51 | result.special = trimToEmpty(matcher.group(4)); 52 | result.prelease = trimToEmpty(matcher.group(5)); 53 | 54 | return result; 55 | } 56 | 57 | throw new IllegalArgumentException("Unsupported version format: " + version); 58 | } 59 | 60 | private static int comparePrerelease(String prerelease1, String prerelease2) { 61 | if (prerelease1.equals(prerelease2)) { 62 | return 0; 63 | } 64 | 65 | if (prerelease1.isEmpty()) { 66 | return 1; 67 | } 68 | 69 | if (prerelease2.isEmpty()) { 70 | return -1; 71 | } 72 | 73 | return prerelease1.compareTo(prerelease2); 74 | } 75 | 76 | private static int compareSpecial(String special1, String special2) { 77 | if (special1.equals(special2)) { 78 | return 0; 79 | } 80 | 81 | if (special1.isEmpty()) { 82 | return -1; 83 | } 84 | 85 | if (special2.isEmpty()) { 86 | return 1; 87 | } 88 | 89 | return special1.compareTo(special2); 90 | } 91 | 92 | @Override 93 | public int compareTo(Version other) { 94 | return compare(this, other); 95 | } 96 | 97 | @Override 98 | public boolean equals(Object obj) { 99 | if (this == obj) { 100 | return true; 101 | } 102 | 103 | if (!(obj instanceof Version)) { 104 | return false; 105 | } 106 | 107 | Version other = (Version) obj; 108 | return compare(this, other) == 0; 109 | } 110 | 111 | public int getMajor() { 112 | return this.major; 113 | } 114 | 115 | public int getMinor() { 116 | return this.minor; 117 | } 118 | 119 | public int getPatch() { 120 | return this.patch; 121 | } 122 | 123 | public String getPrelease() { 124 | return this.prelease; 125 | } 126 | 127 | public String getSpecial() { 128 | return this.special; 129 | } 130 | 131 | @Override 132 | public int hashCode() { 133 | final int prime = 31; 134 | int result = 1; 135 | result = prime * result + this.major; 136 | result = prime * result + this.minor; 137 | result = prime * result + this.patch; 138 | result = prime * result + (this.prelease == null ? 0 : this.prelease.hashCode()); 139 | result = prime * result + (this.special == null ? 0 : this.special.hashCode()); 140 | return result; 141 | } 142 | 143 | public boolean hasSameMajor(Version other) { 144 | return this.major == other.major; 145 | } 146 | 147 | public boolean hasSameMinor(Version other) { 148 | return this.minor == other.minor; 149 | } 150 | 151 | public boolean hasSamePatch(Version other) { 152 | return this.patch == other.patch; 153 | } 154 | 155 | public boolean hasSamePrelease(Version other) { 156 | if (this.prelease == null) { 157 | return other.prelease == null; 158 | } 159 | 160 | return this.prelease.equals(other.prelease); 161 | } 162 | 163 | public Version nextMajor() { 164 | Version result = new Version(); 165 | 166 | result.major = this.major + 1; 167 | result.minor = 0; 168 | result.patch = 0; 169 | result.prelease = ""; 170 | result.special = ""; 171 | 172 | return result; 173 | } 174 | 175 | public void setMajor(int major) { 176 | this.major = major; 177 | } 178 | 179 | public void setMinor(int minor) { 180 | this.minor = minor; 181 | } 182 | 183 | public void setPatch(int patch) { 184 | this.patch = patch; 185 | } 186 | 187 | public void setPrelease(String prelease) { 188 | this.prelease = prelease; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/VersionRequirement.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller; 2 | 3 | import java.util.function.*; 4 | 5 | public class VersionRequirement { 6 | 7 | private Predicate predicate; 8 | 9 | public static VersionRequirement parse(String version) { 10 | VersionRequirement result = new VersionRequirement(); 11 | 12 | if (version.startsWith("^")) { 13 | Version parsedVersion = Version.parse(version.substring(1)); 14 | result.predicate = VersionComparison.atLeast(parsedVersion).and(VersionComparison.below(parsedVersion.nextMajor())); 15 | } else { 16 | Version parsedVersion = Version.parse(version.substring(0)); 17 | result.predicate = VersionComparison.equals(parsedVersion); 18 | } 19 | 20 | return result; 21 | } 22 | 23 | public boolean isSatisfiedBy(Version version) { 24 | return this.predicate.test(version); 25 | } 26 | 27 | public static class VersionComparison implements Predicate { 28 | 29 | private final Version expectedVersion; 30 | private final BiFunction test; 31 | 32 | private VersionComparison(Version expectedVersion, BiFunction test) { 33 | super(); 34 | this.expectedVersion = expectedVersion; 35 | this.test = test; 36 | } 37 | 38 | public static VersionComparison below(Version expectedVersion) { 39 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) > 0); 40 | } 41 | 42 | public static VersionComparison equals(Version expectedVersion) { 43 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) == 0); 44 | } 45 | 46 | public static VersionComparison atLeast(Version expectedVersion) { 47 | return new VersionComparison(expectedVersion, (o1, o2) -> o1.compareTo(o2) <= 0); 48 | } 49 | 50 | @Override 51 | public boolean test(Version version) { 52 | return this.test.apply(this.expectedVersion, version); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityChecker.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.compatibility; 2 | 3 | import java.io.*; 4 | import java.nio.file.*; 5 | import java.util.*; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | import org.springframework.http.ResponseEntity; 9 | 10 | import me.wulfmarius.modinstaller.*; 11 | import me.wulfmarius.modinstaller.rest.RestClient; 12 | import me.wulfmarius.modinstaller.utils.JsonUtils; 13 | 14 | public class CompatibilityChecker { 15 | 16 | private static final String TLD_VERSIONS_URL = "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/tld-versions.json"; 17 | 18 | private final Path basePath; 19 | private final RestClient restClient; 20 | private final CompatibilityState state; 21 | 22 | private final Map compatibilityCache = new ConcurrentHashMap<>(); 23 | 24 | private String currentVersion = Version.VERSION_UNKNOWN; 25 | private Version parsedCurrentVersion; 26 | private CompatibilityVersion currentCompatibilityVersion; 27 | 28 | public CompatibilityChecker(Path basePath, RestClient restClient) { 29 | super(); 30 | 31 | this.basePath = basePath; 32 | this.restClient = restClient; 33 | this.state = this.readState(); 34 | } 35 | 36 | public Compatibility getCompatibility(ModDefinition modDefinition) { 37 | return this.compatibilityCache.computeIfAbsent(modDefinition, this::calculateCompatibility); 38 | } 39 | 40 | public String getCurrentVersion() { 41 | return this.currentVersion; 42 | } 43 | 44 | public CompatibilityState getState() { 45 | return this.state; 46 | } 47 | 48 | public void initialize() { 49 | this.readCurrentVersion(); 50 | this.fetchTldVersions(); 51 | } 52 | 53 | public void invalidate() { 54 | this.compatibilityCache.clear(); 55 | } 56 | 57 | private Compatibility calculateCompatibility(ModDefinition modDefinition) { 58 | if (this.currentCompatibilityVersion == null) { 59 | return Compatibility.UNKNOWN; 60 | } 61 | 62 | CompatibilityVersion modCompatibilityVersion = Optional.ofNullable(modDefinition.getParsedCompatibleWith()) 63 | .map(this.state.getCompatibilityVersions()::floor) 64 | .orElseGet(() -> this.state.getCompatibilityVersions().floor(modDefinition.getReleaseDate())); 65 | 66 | if (this.currentCompatibilityVersion.equals(modCompatibilityVersion)) { 67 | return Compatibility.OK; 68 | } 69 | 70 | return Compatibility.OLD; 71 | } 72 | 73 | private void fetchTldVersions() { 74 | try { 75 | ResponseEntity response = this.restClient.fetch(TLD_VERSIONS_URL, this.state.getEtag()); 76 | if (response.getStatusCode().is2xxSuccessful()) { 77 | this.state.setCompatibilityVersions(this.restClient.deserialize(response, CompatibilityVersions.class, null)); 78 | this.state.setEtag(response.getHeaders().getETag()); 79 | this.currentCompatibilityVersion = this.state.getCompatibilityVersions().floor(this.parsedCurrentVersion); 80 | } 81 | 82 | this.state.setChecked(new Date()); 83 | this.writeState(); 84 | } catch (AbortException e) { 85 | // ignore 86 | } 87 | } 88 | 89 | private Path getStatePath() { 90 | return this.basePath.resolve("compatibility-state.json"); 91 | } 92 | 93 | private Optional getVersionFilePath() { 94 | Optional optional = Optional.of(this.basePath.resolveSibling("tld_Data/StreamingAssets/version.txt")) 95 | .filter(Files::exists); 96 | if (optional.isPresent()) { 97 | return optional; 98 | } 99 | 100 | optional = Optional.of(this.basePath.resolveSibling("tld.app/Contents/Resources/Data/StreamingAssets/version.txt")) 101 | .filter(Files::exists); 102 | return optional; 103 | } 104 | 105 | private void readCurrentVersion() { 106 | Path path = this.getVersionFilePath().orElseThrow(() -> new ModInstallerException("Could not find TLD version file.")); 107 | 108 | try { 109 | List lines = Files.readAllLines(path); 110 | if (lines.isEmpty()) { 111 | throw new ModInstallerException("TLD version file was empty."); 112 | } 113 | 114 | this.currentVersion = lines.get(0).split("\\s")[0]; 115 | this.parsedCurrentVersion = Version.parse(this.currentVersion); 116 | this.currentCompatibilityVersion = this.state.getCompatibilityVersions().floor(this.parsedCurrentVersion); 117 | this.writeState(); 118 | } catch (IllegalArgumentException | IOException e) { 119 | throw new ModInstallerException("Could not read TLD version.", e); 120 | } 121 | } 122 | 123 | private CompatibilityState readState() { 124 | try { 125 | Path path = this.getStatePath(); 126 | if (Files.exists(path)) { 127 | return JsonUtils.deserialize(path, CompatibilityState.class); 128 | } 129 | } catch (IOException e) { 130 | // ignore 131 | } 132 | 133 | try (InputStream inputStream = this.getClass().getResourceAsStream("/default-compatibility-state.json")) { 134 | return JsonUtils.deserialize(inputStream, CompatibilityState.class); 135 | } catch (IOException e) { 136 | // ignore; 137 | } 138 | 139 | return new CompatibilityState(); 140 | } 141 | 142 | private void writeState() { 143 | try { 144 | Path path = this.getStatePath(); 145 | Files.createDirectories(path.getParent()); 146 | JsonUtils.serialize(path, this.state); 147 | } catch (IOException e) { 148 | // ignore 149 | } 150 | } 151 | 152 | public enum Compatibility { 153 | UNKNOWN, OLD, OK; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityState.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.compatibility; 2 | 3 | import java.util.Date; 4 | 5 | public class CompatibilityState { 6 | 7 | private CompatibilityVersions compatibilityVersions; 8 | private String etag; 9 | private Date checked; 10 | 11 | public Date getChecked() { 12 | return this.checked; 13 | } 14 | 15 | public CompatibilityVersions getCompatibilityVersions() { 16 | return this.compatibilityVersions; 17 | } 18 | 19 | public String getEtag() { 20 | return this.etag; 21 | } 22 | 23 | public void setChecked(Date checked) { 24 | this.checked = checked; 25 | } 26 | 27 | public void setCompatibilityVersions(CompatibilityVersions compatibilityVersions) { 28 | this.compatibilityVersions = compatibilityVersions; 29 | } 30 | 31 | public void setEtag(String etag) { 32 | this.etag = etag; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityVersion.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.compatibility; 2 | 3 | import java.util.Date; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | 7 | import me.wulfmarius.modinstaller.Version; 8 | 9 | public class CompatibilityVersion implements Comparable { 10 | 11 | private String version; 12 | private Date date; 13 | 14 | @JsonIgnore 15 | private transient Version parsedVersion; 16 | 17 | @Override 18 | public int compareTo(CompatibilityVersion result) { 19 | return this.getParsedVersion().compareTo(result.getParsedVersion()); 20 | } 21 | 22 | public Date getDate() { 23 | return this.date; 24 | } 25 | 26 | public Version getParsedVersion() { 27 | if (this.parsedVersion == null) { 28 | this.parsedVersion = Version.parse(this.version); 29 | } 30 | 31 | return this.parsedVersion; 32 | } 33 | 34 | public String getVersion() { 35 | return this.version; 36 | } 37 | 38 | public void setDate(Date date) { 39 | this.date = date; 40 | } 41 | 42 | public void setVersion(String version) { 43 | this.version = version; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/compatibility/CompatibilityVersions.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.compatibility; 2 | 3 | import java.util.Date; 4 | import java.util.function.Predicate; 5 | 6 | import me.wulfmarius.modinstaller.Version; 7 | 8 | public class CompatibilityVersions { 9 | 10 | private CompatibilityVersion[] versions; 11 | 12 | public CompatibilityVersion floor(Date date) { 13 | return this.getMax(tldVersion -> tldVersion.getDate().compareTo(date) <= 0); 14 | } 15 | 16 | public CompatibilityVersion floor(Version version) { 17 | return this.getMax(tldVersion -> tldVersion.getParsedVersion().compareTo(version) <= 0); 18 | } 19 | 20 | public CompatibilityVersion[] getVersions() { 21 | return this.versions; 22 | } 23 | 24 | public void setVersions(CompatibilityVersion[] versions) { 25 | this.versions = versions; 26 | } 27 | 28 | private CompatibilityVersion getMax(Predicate filter) { 29 | CompatibilityVersion result = null; 30 | 31 | for (CompatibilityVersion eachTldVersion : this.versions) { 32 | if (!filter.test(eachTldVersion)) { 33 | continue; 34 | } 35 | 36 | if (result == null || eachTldVersion.compareTo(result) > 0) { 37 | result = eachTldVersion; 38 | } 39 | } 40 | 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/DependencyResolution.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | 5 | import me.wulfmarius.modinstaller.*; 6 | 7 | public class DependencyResolution { 8 | 9 | private ModDependencies requested; 10 | private ModDefinition bestMatch; 11 | private Set available = new HashSet<>(); 12 | 13 | public static DependencyResolution empty() { 14 | return new DependencyResolution(); 15 | } 16 | 17 | public Set getAvailable() { 18 | return this.available; 19 | } 20 | 21 | public ModDefinition getBestMatch() { 22 | return this.bestMatch; 23 | } 24 | 25 | public ModDependencies getRequested() { 26 | return this.requested; 27 | } 28 | 29 | public boolean isAvailable() { 30 | return this.available != null && !this.available.isEmpty(); 31 | } 32 | 33 | public boolean isResolved() { 34 | return this.bestMatch != null; 35 | } 36 | 37 | public void setAvailable(Set available) { 38 | this.available = available; 39 | } 40 | 41 | public void setBestMatch(ModDefinition bestMatch) { 42 | this.bestMatch = bestMatch; 43 | } 44 | 45 | public void setRequested(ModDependencies requested) { 46 | this.requested = requested; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/DownloadResponseExtractor.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.io.*; 4 | import java.nio.charset.*; 5 | import java.nio.file.*; 6 | import java.util.regex.*; 7 | 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | import org.springframework.lang.Nullable; 11 | import org.springframework.util.StreamUtils; 12 | import org.springframework.web.client.ResponseExtractor; 13 | 14 | import me.wulfmarius.modinstaller.ProgressListeners; 15 | import me.wulfmarius.modinstaller.utils.StringUtils; 16 | 17 | public class DownloadResponseExtractor implements ResponseExtractor { 18 | 19 | private final Path targetFile; 20 | private final ProgressListeners progressListeners; 21 | 22 | public DownloadResponseExtractor(Path targetFile, ProgressListeners progressListeners) { 23 | super(); 24 | 25 | this.targetFile = targetFile; 26 | this.progressListeners = progressListeners; 27 | } 28 | 29 | private static String getBody(ClientHttpResponse response) throws IOException { 30 | return StreamUtils.copyToString(response.getBody(), getContentTypeCharset(response.getHeaders().getContentType())); 31 | } 32 | 33 | private static Charset getContentTypeCharset(@Nullable MediaType contentType) { 34 | if (contentType != null && contentType.getCharset() != null) { 35 | return contentType.getCharset(); 36 | } 37 | 38 | return StandardCharsets.ISO_8859_1; 39 | } 40 | 41 | @Override 42 | public String extractData(ClientHttpResponse response) throws IOException { 43 | MediaType contentType = response.getHeaders().getContentType(); 44 | if (contentType.isCompatibleWith(MediaType.TEXT_HTML)) { 45 | String body = getBody(response); 46 | Pattern pattern = Pattern.compile("\\Qwindow.location.href=\"\\E(\\Qhttp://www.moddb.com/downloads/\\E.*?)\"", 47 | Pattern.CASE_INSENSITIVE); 48 | Matcher matcher = pattern.matcher(body); 49 | if (matcher.find()) { 50 | return matcher.group(1); 51 | } 52 | 53 | throw new SourceException("Received unexpected text/html response."); 54 | } 55 | 56 | long contentLength = response.getHeaders().getContentLength(); 57 | this.progressListeners.detail(StringUtils.formatByteCount(contentLength)); 58 | 59 | Files.createDirectories(this.targetFile.getParent()); 60 | long copied = 0; 61 | this.progressListeners.stepProgress((int) copied, (int) contentLength); 62 | 63 | byte[] buffer = new byte[4096]; 64 | try (InputStream inputStream = response.getBody(); OutputStream outputStream = Files.newOutputStream(this.targetFile)) { 65 | while (true) { 66 | int count = inputStream.read(buffer); 67 | if (count == -1) { 68 | break; 69 | } 70 | 71 | copied += count; 72 | outputStream.write(buffer, 0, count); 73 | this.progressListeners.stepProgress((int) copied, (int) contentLength); 74 | } 75 | } 76 | 77 | return null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/Installation.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | 5 | import me.wulfmarius.modinstaller.ModDefinition; 6 | 7 | public class Installation { 8 | 9 | private String sourceDefinition; 10 | private String name; 11 | private String version; 12 | private List assets = new ArrayList<>(); 13 | 14 | public void addAsset(String asset) { 15 | if (asset == null) { 16 | return; 17 | } 18 | 19 | this.assets.add(asset); 20 | } 21 | 22 | public List getAssets() { 23 | return this.assets; 24 | } 25 | 26 | public String getDisplayName() { 27 | return this.name + " " + this.version; 28 | } 29 | 30 | public String getName() { 31 | return this.name; 32 | } 33 | 34 | public String getSourceDefinition() { 35 | return this.sourceDefinition; 36 | } 37 | 38 | public String getVersion() { 39 | return this.version; 40 | } 41 | 42 | public boolean isAssetReferenced(String asset) { 43 | return this.assets.contains(asset); 44 | } 45 | 46 | public boolean matches(ModDefinition modDefinition) { 47 | return this.name.equals(modDefinition.getName()) && this.version.equals(modDefinition.getVersion()); 48 | } 49 | 50 | public void setAssets(List assets) { 51 | this.assets = assets; 52 | } 53 | 54 | public void setName(String name) { 55 | this.name = name; 56 | } 57 | 58 | public void setSourceDefinition(String sourceDefinition) { 59 | this.sourceDefinition = sourceDefinition; 60 | } 61 | 62 | public void setVersion(String version) { 63 | this.version = version; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/Installations.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | import java.util.stream.*; 5 | 6 | import me.wulfmarius.modinstaller.ModDefinition; 7 | 8 | public class Installations implements Iterable { 9 | 10 | private List installations = new ArrayList<>(); 11 | 12 | public static Installations create(Installation... installation) { 13 | Installations result = new Installations(); 14 | 15 | if (installation != null) { 16 | for (Installation eachInstallation : installation) { 17 | result.addInstallation(eachInstallation); 18 | } 19 | } 20 | 21 | return result; 22 | } 23 | 24 | public static Installations merge(Installations definitions1, Installations definitions2) { 25 | Installations result = new Installations(); 26 | 27 | result.addInstallations(definitions1); 28 | result.addInstallations(definitions2); 29 | 30 | return result; 31 | } 32 | 33 | private static Collector toInstallations() { 34 | return Collectors.reducing(new Installations(), Installations::create, Installations::merge); 35 | } 36 | 37 | public void addInstallation(Installation installation) { 38 | this.installations.add(installation); 39 | } 40 | 41 | public void addInstallations(Iterable otherInstallations) { 42 | for (Installation eachInstallation : otherInstallations) { 43 | this.addInstallation(eachInstallation); 44 | } 45 | } 46 | 47 | public boolean contains(Installation installation) { 48 | return this.installations.contains(installation); 49 | } 50 | 51 | public boolean contains(ModDefinition modDefinition) { 52 | return this.installations.stream().anyMatch(installation -> installation.matches(modDefinition)); 53 | } 54 | 55 | public List getInstallations() { 56 | return this.installations; 57 | } 58 | 59 | public Installations getInstallations(String name) { 60 | return this.installations.stream().filter(installation -> installation.getName().equals(name)).collect(toInstallations()); 61 | } 62 | 63 | public Installations getInstallationsWithAsset(String asset) { 64 | return this.installations.stream().filter(installation -> installation.isAssetReferenced(asset)).collect(toInstallations()); 65 | } 66 | 67 | public int getSize() { 68 | if (this.installations == null) { 69 | return 0; 70 | } 71 | 72 | return this.installations.size(); 73 | } 74 | 75 | public boolean isEmpty() { 76 | return this.installations == null || this.installations.isEmpty(); 77 | } 78 | 79 | @Override 80 | public Iterator iterator() { 81 | return this.installations.iterator(); 82 | } 83 | 84 | public void remove(Installation installation) { 85 | this.installations.remove(installation); 86 | } 87 | 88 | public void remove(ModDefinition modDefinition) { 89 | this.installations.removeIf(installation -> installation.matches(modDefinition)); 90 | } 91 | 92 | public void setInstallations(List installations) { 93 | this.installations = installations; 94 | } 95 | 96 | public Stream stream() { 97 | return this.installations.stream(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.time.Instant; 4 | 5 | public class RateLimitException extends RuntimeException { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | private final Instant reset; 10 | 11 | public RateLimitException(Instant reset) { 12 | super(); 13 | this.reset = reset; 14 | } 15 | 16 | public Instant getReset() { 17 | return this.reset; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/Repository.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import static me.wulfmarius.modinstaller.repository.SourceFactory.PARAMETER_ETAG; 4 | 5 | import java.io.*; 6 | import java.nio.file.*; 7 | import java.util.*; 8 | import java.util.stream.Collectors; 9 | 10 | import org.springframework.http.*; 11 | import org.springframework.util.StringUtils; 12 | 13 | import me.wulfmarius.modinstaller.*; 14 | import me.wulfmarius.modinstaller.ProgressListener.StepType; 15 | import me.wulfmarius.modinstaller.repository.source.*; 16 | import me.wulfmarius.modinstaller.rest.RestClient; 17 | import me.wulfmarius.modinstaller.utils.JsonUtils; 18 | 19 | public class Repository { 20 | 21 | private static final String SNAPSHOT_URL = "https://raw.githubusercontent.com/WulfMarius/Mod-Installer/master/src/main/resources/default-sources.json"; 22 | 23 | private final Path basePath; 24 | 25 | private final Sources sources = new Sources(); 26 | private final List sourceFactories = new ArrayList<>(); 27 | private final ProgressListeners progressListeners = new ProgressListeners(); 28 | private final SourcesChangedListeners sourcesChangedListeners = new SourcesChangedListeners(); 29 | 30 | public Repository(Path basePath) { 31 | super(); 32 | 33 | this.basePath = basePath; 34 | try { 35 | Files.createDirectories(basePath); 36 | } catch (IOException e) { 37 | throw new RepositoryException("Could not create base path " + basePath + ".", e); 38 | } 39 | } 40 | 41 | public static String getFileName(Asset asset) { 42 | String url = asset.getUrl(); 43 | 44 | int index = url.indexOf("?"); 45 | if (index != -1) { 46 | url = url.substring(0, index); 47 | } 48 | 49 | index = url.lastIndexOf('/'); 50 | if (index == url.length() - 1) { 51 | url = url.substring(0, url.length() - 1); 52 | index = url.lastIndexOf('/'); 53 | } 54 | 55 | if (index != -1) { 56 | url = url.substring(index + 1); 57 | } 58 | 59 | if (StringUtils.isEmpty(asset.getType())) { 60 | return url; 61 | } 62 | 63 | return url; 64 | } 65 | 66 | public void addProgressListener(ProgressListener listener) { 67 | this.progressListeners.addListener(listener); 68 | } 69 | 70 | public void addSourcesChangedListener(SourcesChangedListener listener) { 71 | this.sourcesChangedListeners.addListener(listener); 72 | } 73 | 74 | public void downloadAssets(ModDefinition modDefinition) { 75 | Asset[] assets = modDefinition.getAssets(); 76 | 77 | for (Asset eachAsset : assets) { 78 | Path assetPath = this.getAssetPath(modDefinition, eachAsset); 79 | if (Files.notExists(assetPath)) { 80 | RestClient.getInstance().downloadAsset(eachAsset.getUrl(), assetPath, this.progressListeners); 81 | } 82 | } 83 | } 84 | 85 | public Path getAssetPath(ModDefinition modDefinition, Asset asset) { 86 | return this.basePath.resolve(modDefinition.getName()).resolve(modDefinition.getVersion()).resolve(getFileName(asset)); 87 | } 88 | 89 | public List getLatestVersions() { 90 | return this.getSources().stream().flatMap(Source::getLatestVersions).collect(Collectors.toList()); 91 | } 92 | 93 | public ModDefinitions getMatching(ModDependency modDependency) { 94 | ModDefinitions result = new ModDefinitions(); 95 | 96 | for (Source eachSource : this.sources) { 97 | result.addModDefinitions(eachSource.getMatchingDefinitions(modDependency)); 98 | } 99 | 100 | return result; 101 | } 102 | 103 | public Optional getModDefinition(String name, String version) { 104 | for (Source eachSource : this.sources) { 105 | Optional modDefinition = eachSource.getModDefinition(name, version); 106 | if (modDefinition.isPresent()) { 107 | return modDefinition; 108 | } 109 | } 110 | 111 | return Optional.empty(); 112 | } 113 | 114 | public List getModDefinitions(String name) { 115 | return this.sources.stream() 116 | .flatMap(Source::getModDefinitionStream) 117 | .filter(modDefinition -> modDefinition.getName().equals(name)) 118 | .sorted(ModDefinition::latest) 119 | .collect(Collectors.toList()); 120 | } 121 | 122 | public Sources getSources() { 123 | return this.sources; 124 | } 125 | 126 | public void initialize() { 127 | this.sourceFactories.add(new GithubSourceFactory(RestClient.getInstance())); 128 | this.sourceFactories.add(new DirectSourceFactory(RestClient.getInstance())); 129 | this.sourceFactories.add(new FileSourceFactory()); 130 | 131 | Sources savedSources = this.readSources(); 132 | if (!savedSources.isEmpty()) { 133 | this.sources.addSources(savedSources); 134 | this.sources.setLastUpdate(savedSources.getLastUpdate()); 135 | this.sources.setSnapshotETag(savedSources.getSnapshotETag()); 136 | this.sourcesChangedListeners.changed(); 137 | } 138 | } 139 | 140 | public void invalidateSources() { 141 | for (Source eachSource : this.sources) { 142 | eachSource.removeParameter(SourceFactory.PARAMETER_ETAG); 143 | } 144 | } 145 | 146 | public void refreshSnapshot() { 147 | ResponseEntity response = RestClient.getInstance().fetch(SNAPSHOT_URL, this.sources.getSnapshotETag()); 148 | 149 | if (response.getStatusCode() == HttpStatus.NOT_MODIFIED) { 150 | return; 151 | } 152 | 153 | if (!response.getStatusCode().is2xxSuccessful()) { 154 | this.progressListeners.error("Could not find snapshot: " + SNAPSHOT_URL + " " + response.getStatusCodeValue() + "/" 155 | + response.getStatusCode().getReasonPhrase()); 156 | return; 157 | } 158 | 159 | Sources snapshot = RestClient.getInstance().deserialize(response, Sources.class, Sources::new); 160 | this.applySnapshot(snapshot); 161 | 162 | this.sources.setSnapshotETag(response.getHeaders().getETag()); 163 | this.writeSources(); 164 | } 165 | 166 | public void refreshSources() { 167 | String changes = null; 168 | 169 | try { 170 | this.progressListeners.started("Refreshing Sources"); 171 | 172 | List previousLatestVersions = this.getLatestVersions(); 173 | 174 | this.performRefreshSources(); 175 | 176 | List currentLatestVersions = this.getLatestVersions(); 177 | currentLatestVersions.removeAll(previousLatestVersions); 178 | if (!currentLatestVersions.isEmpty()) { 179 | changes = currentLatestVersions.stream().map(ModDefinition::getName).collect( 180 | Collectors.joining("\n\t", "\n\nThe following mods were added/updated:\n\t", "\n")); 181 | } else { 182 | changes = "\n\nNo changes found"; 183 | } 184 | } catch (AbortException e) { 185 | this.progressListeners.error(e.getMessage()); 186 | this.progressListeners.detail("Aborting now."); 187 | } finally { 188 | this.writeSources(); 189 | this.progressListeners.finished(changes); 190 | } 191 | } 192 | 193 | public void registerSource(String definition) { 194 | try { 195 | this.progressListeners.started("Add " + definition); 196 | this.performRegisterSource(definition); 197 | } catch (AbortException e) { 198 | this.progressListeners.error(e.getMessage()); 199 | this.progressListeners.detail("Aborting now."); 200 | } finally { 201 | this.writeSources(); 202 | this.progressListeners.stepProgress(1, 1); 203 | this.progressListeners.finished(); 204 | } 205 | } 206 | 207 | public void removeProgressListener(ProgressListener listener) { 208 | this.progressListeners.removeListener(listener); 209 | } 210 | 211 | public void removeSourcesChangedListener(SourcesChangedListener listener) { 212 | this.sourcesChangedListeners.removeListener(listener); 213 | } 214 | 215 | private void addSource(Source source) { 216 | this.sources.addSource(source); 217 | } 218 | 219 | private void applySnapshot(Sources snapshot) { 220 | for (Source eachSnapshotSource : snapshot) { 221 | if (!this.sources.contains(eachSnapshotSource.getDefinition())) { 222 | this.progressListeners.detail("Adding " + eachSnapshotSource.getDefinition()); 223 | this.addSource(eachSnapshotSource); 224 | continue; 225 | } 226 | 227 | Date now = new Date(0); 228 | this.sources.stream() 229 | .filter(eachSource -> eachSource.getDefinition().equals(eachSnapshotSource.getDefinition())) 230 | .filter(eachSource -> !eachSource.getParameter(PARAMETER_ETAG).equals(eachSnapshotSource.getParameter(PARAMETER_ETAG))) 231 | .filter(eachSource -> !eachSource.getLastUpdated().orElse(now).after(eachSnapshotSource.getLastUpdated().orElse(now))) 232 | .findFirst() 233 | .ifPresent(eachSource -> { 234 | this.progressListeners.detail("Updating " + eachSource.getDefinition()); 235 | eachSource.update(eachSnapshotSource); 236 | }); 237 | } 238 | } 239 | 240 | private Source createSource(String sourceDefinition, Map parameters) { 241 | for (SourceFactory eachSourceFactory : this.sourceFactories) { 242 | if (!eachSourceFactory.isSupportedSource(sourceDefinition)) { 243 | continue; 244 | } 245 | 246 | this.progressListeners.detail("Loading definition"); 247 | return eachSourceFactory.create(sourceDefinition, parameters); 248 | } 249 | 250 | throw new SourceException("Unsupported source '" + sourceDefinition + "'."); 251 | } 252 | 253 | private Path getSourcesPath() { 254 | return this.basePath.resolve("sources.json"); 255 | } 256 | 257 | private void performRefreshSources() { 258 | int refreshed = 0; 259 | int total = this.sources.size(); 260 | 261 | for (int i = 0; i < total; i++) { 262 | try { 263 | this.refreshSource(this.sources.getSources().get(i)); 264 | } catch (AbortException e) { 265 | throw e; 266 | } catch (Exception e) { 267 | this.progressListeners.error(e.getMessage()); 268 | } 269 | this.progressListeners.stepProgress(++refreshed, total); 270 | } 271 | } 272 | 273 | private void performRegisterSource(String definition) { 274 | this.progressListeners.stepStarted(definition, StepType.ADD); 275 | 276 | if (this.sources.contains(definition)) { 277 | this.progressListeners.detail("Already present."); 278 | return; 279 | } 280 | 281 | try { 282 | Source source = this.createSource(definition, Collections.emptyMap()); 283 | this.registerDefinitions(source); 284 | this.addSource(source); 285 | } catch (AbortException e) { 286 | throw e; 287 | } catch (RuntimeException e) { 288 | this.progressListeners.detail("Could not register source: " + e); 289 | } 290 | } 291 | 292 | private Sources readSources() { 293 | try { 294 | Path sourcesPath = this.getSourcesPath(); 295 | if (Files.exists(sourcesPath)) { 296 | return JsonUtils.deserialize(sourcesPath, Sources.class); 297 | } 298 | } catch (IOException e) { 299 | throw new RepositoryException("Failed to read sources.", e); 300 | } 301 | 302 | try (InputStream inputStream = this.getClass().getResourceAsStream("/default-sources.json")) { 303 | return JsonUtils.deserialize(inputStream, Sources.class); 304 | } catch (IOException e) { 305 | // ignore 306 | } 307 | 308 | return new Sources(); 309 | } 310 | 311 | private boolean refreshSource(Source source) { 312 | this.progressListeners.stepStarted(source.getDefinition(), StepType.REFRESH); 313 | 314 | Source refreshedSource = this.createSource(source.getDefinition(), source.getParameters()); 315 | if (refreshedSource.hasParameterValue(SourceFactory.PARAMETER_UNMODIFIED, "true")) { 316 | this.progressListeners.detail("Unmodified"); 317 | 318 | // it may be possible that this source's definitions were not added last time (because of an error) 319 | // so retry to register them now 320 | this.registerDefinitions(source); 321 | 322 | return false; 323 | } 324 | 325 | source.update(refreshedSource); 326 | this.registerDefinitions(refreshedSource); 327 | 328 | this.progressListeners.detail("Updated"); 329 | return true; 330 | } 331 | 332 | private void registerDefinitions(Source source) { 333 | String[] definitions = source.getDefinitions(); 334 | if (definitions == null) { 335 | return; 336 | } 337 | 338 | for (String eachDefinition : definitions) { 339 | if (this.sources.contains(eachDefinition)) { 340 | continue; 341 | } 342 | 343 | this.performRegisterSource(eachDefinition); 344 | } 345 | } 346 | 347 | private void writeSources() { 348 | try { 349 | this.sources.setLastUpdate(new Date()); 350 | JsonUtils.serialize(this.getSourcesPath(), this.sources); 351 | this.sourcesChangedListeners.changed(); 352 | } catch (IOException e) { 353 | this.progressListeners.error("Could not save sources: " + e); 354 | } 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/RepositoryException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import me.wulfmarius.modinstaller.ModInstallerException; 4 | 5 | public class RepositoryException extends ModInstallerException { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public RepositoryException(String message) { 10 | super(message); 11 | } 12 | 13 | public RepositoryException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/Source.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | import java.util.stream.*; 5 | 6 | import org.springframework.util.StringUtils; 7 | 8 | import me.wulfmarius.modinstaller.*; 9 | 10 | public class Source { 11 | 12 | public static final String VERSION = "3"; 13 | 14 | private String definition; 15 | private String name; 16 | private String url; 17 | private String description; 18 | private String[] definitions; 19 | 20 | private final Map parameters = new HashMap<>(); 21 | 22 | private ModDefinitions modDefinitions = new ModDefinitions(); 23 | 24 | public static Source from(String definition, SourceDescription sourceDescription) { 25 | Source result = new Source(); 26 | 27 | result.setDefinition(definition); 28 | result.setName(sourceDescription.getName()); 29 | result.setUrl(sourceDescription.getUrl()); 30 | result.setDescription(sourceDescription.getDescription()); 31 | result.setDefinitions(sourceDescription.getDefinitions()); 32 | result.parameters.putAll(sourceDescription.getParameters()); 33 | result.createModDefinitions(sourceDescription.getReleases()); 34 | 35 | return result; 36 | } 37 | 38 | public String getDefinition() { 39 | return this.definition; 40 | } 41 | 42 | public String[] getDefinitions() { 43 | return this.definitions; 44 | } 45 | 46 | public String getDescription() { 47 | return this.description; 48 | } 49 | 50 | public Optional getLastUpdated() { 51 | return this.getModDefinitionStream().map(ModDefinition::getLastUpdated).max(Date::compareTo); 52 | } 53 | 54 | public Stream getLatestVersions() { 55 | Map> collect = this.modDefinitions.stream() 56 | .collect(Collectors.groupingBy(ModDefinition::getName, Collectors.minBy(ModDefinition::latest))); 57 | return collect.values().stream().map(Optional::get); 58 | } 59 | 60 | public ModDefinitions getMatchingDefinitions(ModDependency dependency) { 61 | return this.modDefinitions.getMatchingDefinitions(dependency); 62 | } 63 | 64 | public Optional getModDefinition(String modDefinitionName, String modDefinitionVersion) { 65 | return this.modDefinitions.getModDefinition(modDefinitionName, modDefinitionVersion); 66 | } 67 | 68 | public ModDefinitions getModDefinitions() { 69 | return this.modDefinitions; 70 | } 71 | 72 | public Stream getModDefinitionStream() { 73 | return this.modDefinitions.stream(); 74 | } 75 | 76 | public String getName() { 77 | return this.name; 78 | } 79 | 80 | public String getParameter(String parameterName) { 81 | if (this.parameters == null) { 82 | return null; 83 | } 84 | 85 | return this.parameters.get(parameterName); 86 | } 87 | 88 | public Map getParameters() { 89 | return this.parameters; 90 | } 91 | 92 | public String getUrl() { 93 | return this.url; 94 | } 95 | 96 | public boolean hasParameterValue(String parameterName, String value) { 97 | if (value == null) { 98 | return this.getParameter(parameterName) == null; 99 | } 100 | 101 | return value.equals(this.getParameter(parameterName)); 102 | } 103 | 104 | public void removeParameter(String parameterName) { 105 | this.parameters.remove(parameterName); 106 | } 107 | 108 | public void setDefinition(String definition) { 109 | this.definition = definition; 110 | } 111 | 112 | public void setDefinitions(String[] definitions) { 113 | this.definitions = definitions; 114 | } 115 | 116 | public void setDescription(String description) { 117 | this.description = description; 118 | } 119 | 120 | public void setModDefinitions(ModDefinitions modDefinitions) { 121 | this.modDefinitions = modDefinitions; 122 | } 123 | 124 | public void setName(String name) { 125 | this.name = name; 126 | } 127 | 128 | public void setUrl(String url) { 129 | this.url = url; 130 | } 131 | 132 | public void update(Source refreshedSource) { 133 | this.name = refreshedSource.name; 134 | this.description = refreshedSource.description; 135 | this.modDefinitions = refreshedSource.modDefinitions; 136 | 137 | this.parameters.clear(); 138 | this.parameters.putAll(refreshedSource.parameters); 139 | } 140 | 141 | private void createModDefinitions(ModDefinition[] releases) { 142 | if (releases == null) { 143 | return; 144 | } 145 | 146 | for (ModDefinition eachModDefinition : releases) { 147 | if (StringUtils.isEmpty(eachModDefinition.getName())) { 148 | eachModDefinition.setName(this.name); 149 | } 150 | 151 | if (StringUtils.isEmpty(eachModDefinition.getUrl())) { 152 | eachModDefinition.setUrl(this.url); 153 | } 154 | if (StringUtils.isEmpty(eachModDefinition.getUrl())) { 155 | eachModDefinition.setUrl(this.definition); 156 | } 157 | 158 | if (StringUtils.isEmpty(eachModDefinition.getDescription())) { 159 | eachModDefinition.setDescription(this.description); 160 | } 161 | 162 | eachModDefinition.setLastUpdated(new Date()); 163 | 164 | this.modDefinitions.addModDefinition(eachModDefinition); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/SourceDescription.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | 5 | import me.wulfmarius.modinstaller.ModDefinition; 6 | 7 | public class SourceDescription { 8 | 9 | private String name; 10 | private String author; 11 | private String url; 12 | private String description; 13 | private ModDefinition[] releases; 14 | private String[] definitions; 15 | private Map parameters = new HashMap<>(); 16 | 17 | public String getAuthor() { 18 | return this.author; 19 | } 20 | 21 | public String[] getDefinitions() { 22 | return this.definitions; 23 | } 24 | 25 | public String getDescription() { 26 | return this.description; 27 | } 28 | 29 | public String getName() { 30 | return this.name; 31 | } 32 | 33 | public Map getParameters() { 34 | return this.parameters; 35 | } 36 | 37 | public ModDefinition[] getReleases() { 38 | return this.releases; 39 | } 40 | 41 | public String getUrl() { 42 | return this.url; 43 | } 44 | 45 | public void setAuthor(String author) { 46 | this.author = author; 47 | } 48 | 49 | public void setDefinitions(String[] definitions) { 50 | this.definitions = definitions; 51 | } 52 | 53 | public void setDescription(String description) { 54 | this.description = description; 55 | } 56 | 57 | public void setName(String name) { 58 | this.name = name; 59 | } 60 | 61 | public void setParameter(String name, String value) { 62 | this.parameters.put(name, value); 63 | } 64 | 65 | public void setParameters(Map parameters) { 66 | this.parameters = parameters; 67 | } 68 | 69 | public void setReleases(ModDefinition[] releases) { 70 | this.releases = releases; 71 | } 72 | 73 | public void setUrl(String url) { 74 | this.url = url; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/SourceException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | public class SourceException extends RepositoryException { 4 | 5 | private static final long serialVersionUID = 1L; 6 | 7 | public SourceException(String message) { 8 | super(message); 9 | } 10 | 11 | public SourceException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/SourceFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.Map; 4 | 5 | public interface SourceFactory { 6 | 7 | String PARAMETER_UNMODIFIED = "unmodified"; 8 | String PARAMETER_ETAG = "etag"; 9 | String PARAMETER_VERSION = "version"; 10 | 11 | Source create(String sourceDefinition, Map parameters); 12 | 13 | boolean isSupportedSource(String sourceDefinition); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/Sources.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository; 2 | 3 | import java.util.*; 4 | import java.util.stream.Stream; 5 | 6 | public class Sources implements Iterable { 7 | 8 | private List sources = new ArrayList<>(); 9 | 10 | private Date lastUpdate; 11 | private String snapshotETag; 12 | 13 | public void addSource(Source source) { 14 | this.sources.add(source); 15 | } 16 | 17 | public void addSources(Iterable otherSources) { 18 | for (Source eachSource : otherSources) { 19 | this.addSource(eachSource); 20 | } 21 | } 22 | 23 | public boolean contains(String definition) { 24 | return this.sources.stream().anyMatch(source -> definition.equalsIgnoreCase(source.getDefinition())); 25 | } 26 | 27 | public Date getLastUpdate() { 28 | return this.lastUpdate; 29 | } 30 | 31 | public String getSnapshotETag() { 32 | return this.snapshotETag; 33 | } 34 | 35 | public List getSources() { 36 | return this.sources; 37 | } 38 | 39 | public boolean isEmpty() { 40 | return this.sources == null || this.sources.isEmpty(); 41 | } 42 | 43 | @Override 44 | public Iterator iterator() { 45 | return this.sources.iterator(); 46 | } 47 | 48 | public void removeSource(Source source) { 49 | this.sources.remove(source); 50 | } 51 | 52 | public void setLastUpdate(Date lastUpdate) { 53 | this.lastUpdate = lastUpdate; 54 | } 55 | 56 | public void setSnapshotETag(String snapshotETag) { 57 | this.snapshotETag = snapshotETag; 58 | } 59 | 60 | public void setSources(List sources) { 61 | this.sources = sources; 62 | } 63 | 64 | public int size() { 65 | if (this.sources == null) { 66 | return 0; 67 | } 68 | 69 | return this.sources.size(); 70 | } 71 | 72 | public Stream stream() { 73 | if (this.sources == null) { 74 | return Stream.empty(); 75 | } 76 | 77 | return this.sources.stream(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/AbstractSourceFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.util.StringUtils; 7 | 8 | import me.wulfmarius.modinstaller.repository.*; 9 | import me.wulfmarius.modinstaller.rest.RestClient; 10 | 11 | public abstract class AbstractSourceFactory implements SourceFactory { 12 | 13 | protected final RestClient restClient; 14 | 15 | protected AbstractSourceFactory(RestClient restClient) { 16 | super(); 17 | this.restClient = restClient; 18 | } 19 | 20 | @Override 21 | public final Source create(String definition, Map parameters) { 22 | if (!this.isSupportedSource(definition)) { 23 | throw new IllegalArgumentException("Unsupported source definition " + definition); 24 | } 25 | 26 | SourceDescription sourceDescription = this.getSourceDescription(definition, parameters); 27 | this.postProcessSourceDescription(definition, sourceDescription); 28 | return Source.from(definition, sourceDescription); 29 | } 30 | 31 | protected abstract String getDefinitionsUrl(String sourceDefinition); 32 | 33 | protected SourceDescription getSourceDescription(String sourceDefinition, Map parameters) { 34 | String url = this.getDefinitionsUrl(sourceDefinition); 35 | 36 | ResponseEntity response = this.restClient.fetch(url, parameters.get(PARAMETER_ETAG)); 37 | if (!response.getStatusCode().isError()) { 38 | SourceDescription result = this.restClient.deserialize(response, SourceDescription.class, 39 | this::createUnmodifiedSourceDescription); 40 | 41 | result.setParameter(PARAMETER_ETAG, response.getHeaders().getETag()); 42 | result.setParameter(PARAMETER_VERSION, Source.VERSION); 43 | 44 | return result; 45 | } 46 | 47 | throw new SourceException("Could not read source description: " + response.getStatusCodeValue() + ", " + response.getBody()); 48 | } 49 | 50 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) { 51 | if (StringUtils.isEmpty(sourceDescription.getUrl())) { 52 | sourceDescription.setUrl(definition); 53 | } 54 | } 55 | 56 | private SourceDescription createUnmodifiedSourceDescription() { 57 | SourceDescription result = new SourceDescription(); 58 | result.setParameter(PARAMETER_UNMODIFIED, "true"); 59 | return result; 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/DirectSourceFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import me.wulfmarius.modinstaller.ModDefinition; 4 | import me.wulfmarius.modinstaller.repository.SourceDescription; 5 | import me.wulfmarius.modinstaller.rest.RestClient; 6 | 7 | public class DirectSourceFactory extends AbstractSourceFactory { 8 | 9 | public DirectSourceFactory(RestClient restClient) { 10 | super(restClient); 11 | } 12 | 13 | @Override 14 | public boolean isSupportedSource(String definition) { 15 | return definition.startsWith("http://") || definition.startsWith("https://"); 16 | } 17 | 18 | @Override 19 | protected String getDefinitionsUrl(String sourceDefinition) { 20 | return sourceDefinition; 21 | } 22 | 23 | @Override 24 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) { 25 | super.postProcessSourceDescription(definition, sourceDescription); 26 | 27 | ModDefinition[] releases = sourceDescription.getReleases(); 28 | if (releases == null) { 29 | return; 30 | } 31 | 32 | for (ModDefinition eachRelease : releases) { 33 | if (eachRelease.getAuthor() == null) { 34 | eachRelease.setAuthor(sourceDescription.getAuthor()); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/FileSourceFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Paths; 5 | import java.util.Map; 6 | 7 | import me.wulfmarius.modinstaller.repository.*; 8 | import me.wulfmarius.modinstaller.utils.JsonUtils; 9 | 10 | public class FileSourceFactory extends AbstractSourceFactory { 11 | 12 | public FileSourceFactory() { 13 | super(null); 14 | } 15 | 16 | @Override 17 | public boolean isSupportedSource(String sourceDefinition) { 18 | return true; 19 | } 20 | 21 | @Override 22 | protected String getDefinitionsUrl(String sourceDefinition) { 23 | return null; 24 | } 25 | 26 | @Override 27 | protected SourceDescription getSourceDescription(String sourceDefinition, Map parameters) { 28 | try { 29 | return JsonUtils.deserialize(Paths.get(sourceDefinition), SourceDescription.class); 30 | } catch (IOException e) { 31 | throw new SourceException("Could not read source description: " + e.getMessage()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/GithubAsset.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | 5 | import me.wulfmarius.modinstaller.Asset; 6 | 7 | @JsonIgnoreProperties(ignoreUnknown = true) 8 | public class GithubAsset { 9 | 10 | private String name; 11 | 12 | @JsonProperty("browser_download_url") 13 | private String downloadUrl; 14 | 15 | public String getDownloadUrl() { 16 | return this.downloadUrl; 17 | } 18 | 19 | public String getName() { 20 | return this.name; 21 | } 22 | 23 | public void setDownloadUrl(String downloadUrl) { 24 | this.downloadUrl = downloadUrl; 25 | } 26 | 27 | public void setName(String name) { 28 | this.name = name; 29 | } 30 | 31 | public Asset toAsset() { 32 | return Asset.withUrl(this.downloadUrl); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/GithubAuthor.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | @JsonIgnoreProperties(ignoreUnknown = true) 6 | public class GithubAuthor { 7 | 8 | private String login; 9 | 10 | public String getLogin() { 11 | return this.login; 12 | } 13 | 14 | public void setLogin(String login) { 15 | this.login = login; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/GithubRelease.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import java.util.Date; 4 | 5 | import com.fasterxml.jackson.annotation.*; 6 | 7 | import me.wulfmarius.modinstaller.Version; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class GithubRelease { 11 | 12 | private String name; 13 | 14 | @JsonProperty("tag_name") 15 | private String tag; 16 | 17 | @JsonProperty("html_url") 18 | private String url; 19 | 20 | @JsonProperty("published_at") 21 | private Date date; 22 | 23 | private GithubAsset[] assets; 24 | 25 | private GithubAuthor author; 26 | 27 | private String body; 28 | 29 | public GithubAsset[] getAssets() { 30 | return this.assets; 31 | } 32 | 33 | public GithubAuthor getAuthor() { 34 | return this.author; 35 | } 36 | 37 | public String getBody() { 38 | return this.body; 39 | } 40 | 41 | public Date getDate() { 42 | return this.date; 43 | } 44 | 45 | public String getName() { 46 | return this.name; 47 | } 48 | 49 | public String getTag() { 50 | return this.tag; 51 | } 52 | 53 | public String getUrl() { 54 | return this.url; 55 | } 56 | 57 | public boolean hasMatchingTag(String tagName) { 58 | if (this.tag == null) { 59 | return false; 60 | } 61 | 62 | if (this.tag.equalsIgnoreCase(tagName)) { 63 | return true; 64 | } 65 | 66 | try { 67 | return Version.parse(this.tag).equals(Version.parse(tagName)); 68 | } catch (Exception e) { 69 | return false; 70 | } 71 | } 72 | 73 | public void setAssets(GithubAsset[] assets) { 74 | this.assets = assets; 75 | } 76 | 77 | public void setAuthor(GithubAuthor author) { 78 | this.author = author; 79 | } 80 | 81 | public void setBody(String body) { 82 | this.body = body; 83 | } 84 | 85 | public void setDate(Date date) { 86 | this.date = date; 87 | } 88 | 89 | public void setName(String name) { 90 | this.name = name; 91 | } 92 | 93 | public void setTag(String tag) { 94 | this.tag = tag; 95 | } 96 | 97 | public void setUrl(String url) { 98 | this.url = url; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/repository/source/GithubSourceFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.repository.source; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.*; 5 | import java.util.regex.*; 6 | 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.util.StringUtils; 9 | 10 | import me.wulfmarius.modinstaller.*; 11 | import me.wulfmarius.modinstaller.repository.SourceDescription; 12 | import me.wulfmarius.modinstaller.rest.RestClient; 13 | 14 | public class GithubSourceFactory extends AbstractSourceFactory { 15 | 16 | public static final String PARAMETER_USER = "user"; 17 | 18 | private static final Pattern SOURCE_PATTERN = Pattern.compile("\\Qhttps://github.com/\\E([A-Z0-9-]+)/([A-Z0-9-_]+)/?", 19 | Pattern.CASE_INSENSITIVE); 20 | 21 | public GithubSourceFactory(RestClient restClient) { 22 | super(restClient); 23 | } 24 | 25 | @Override 26 | public boolean isSupportedSource(String sourceDefinition) { 27 | return SOURCE_PATTERN.matcher(sourceDefinition).matches(); 28 | } 29 | 30 | @Override 31 | protected String getDefinitionsUrl(String sourceDefinition) { 32 | Matcher matcher = SOURCE_PATTERN.matcher(sourceDefinition); 33 | if (!matcher.matches()) { 34 | throw new IllegalArgumentException("Unsupported source definition " + sourceDefinition); 35 | } 36 | 37 | return MessageFormat.format("https://raw.githubusercontent.com/{0}/{1}/master/mod-installer-description.json", 38 | matcher.group(1), 39 | matcher.group(2)); 40 | } 41 | 42 | protected GithubRelease[] getGithubReleases(String definition) { 43 | String url = definition.replace("//github.com/", "//api.github.com/repos/") + "/releases"; 44 | ResponseEntity response = this.restClient.fetch(url, null); 45 | return this.restClient.deserialize(response, GithubRelease[].class, () -> new GithubRelease[0]); 46 | } 47 | 48 | @Override 49 | protected void postProcessSourceDescription(String definition, SourceDescription sourceDescription) { 50 | super.postProcessSourceDescription(definition, sourceDescription); 51 | 52 | ModDefinition[] releases = sourceDescription.getReleases(); 53 | if (releases == null) { 54 | return; 55 | } 56 | 57 | ReleaseProvider releaseProvider = new ReleaseProvider(definition); 58 | 59 | for (ModDefinition eachRelease : releases) { 60 | if (StringUtils.isEmpty(eachRelease.getUrl())) { 61 | eachRelease.setUrl(releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getUrl).orElse( 62 | definition + "/releases/tag/" + eachRelease.getVersion())); 63 | } 64 | 65 | if (StringUtils.isEmpty(eachRelease.getChanges())) { 66 | eachRelease.setChanges(releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getBody).orElse("")); 67 | } 68 | 69 | if (eachRelease.getAssets() == null || eachRelease.getAssets().length == 0) { 70 | eachRelease.setAssets(releaseProvider.getRelease(eachRelease.getVersion()) 71 | .map(GithubRelease::getAssets) 72 | .map(assets -> Arrays.stream(assets).map(GithubAsset::toAsset).toArray(Asset[]::new)) 73 | .orElse(new Asset[0])); 74 | } 75 | 76 | if (eachRelease.getReleaseDate() == null) { 77 | eachRelease.setReleaseDate( 78 | releaseProvider.getRelease(eachRelease.getVersion()).map(GithubRelease::getDate).orElse(new Date())); 79 | } 80 | 81 | if (eachRelease.getAuthor() == null) { 82 | eachRelease.setAuthor(releaseProvider.getRelease(eachRelease.getVersion()) 83 | .map(GithubRelease::getAuthor) 84 | .map(GithubAuthor::getLogin) 85 | .orElse(sourceDescription.getAuthor())); 86 | } 87 | 88 | if (eachRelease.getAuthor() == null) { 89 | Matcher matcher = SOURCE_PATTERN.matcher(definition); 90 | if (matcher.matches()) { 91 | eachRelease.setAuthor(matcher.group(1)); 92 | } 93 | } 94 | } 95 | } 96 | 97 | private class ReleaseProvider { 98 | 99 | private final String definition; 100 | private GithubRelease[] githubReleases = null; 101 | 102 | public ReleaseProvider(String definition) { 103 | super(); 104 | this.definition = definition; 105 | } 106 | 107 | public Optional getRelease(String version) { 108 | if (this.githubReleases == null) { 109 | this.githubReleases = GithubSourceFactory.this.getGithubReleases(this.definition); 110 | } 111 | 112 | return Arrays.stream(this.githubReleases).filter(release -> release.hasMatchingTag(version)).findFirst(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/rest/HostUnreachableException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.rest; 2 | 3 | import me.wulfmarius.modinstaller.AbortException; 4 | 5 | public class HostUnreachableException extends AbortException { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public HostUnreachableException(String host) { 10 | super("Unknown host " + host + ". Are you offline?"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/rest/RateLimitException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.rest; 2 | 3 | import java.time.*; 4 | import java.time.format.*; 5 | 6 | import me.wulfmarius.modinstaller.AbortException; 7 | 8 | public class RateLimitException extends AbortException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | private final Instant reset; 13 | 14 | public RateLimitException(Instant reset) { 15 | super("RATE LIMIT REACHED. Please try again after " 16 | + DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM).format(reset.atZone(ZoneId.systemDefault()))); 17 | this.reset = reset; 18 | } 19 | 20 | public Instant getReset() { 21 | return this.reset; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/rest/RestClient.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.rest; 2 | 3 | import java.io.*; 4 | import java.net.UnknownHostException; 5 | import java.nio.charset.*; 6 | import java.nio.file.Path; 7 | import java.time.Instant; 8 | import java.util.Arrays; 9 | import java.util.function.Supplier; 10 | import java.util.zip.GZIPInputStream; 11 | 12 | import org.springframework.core.NestedRuntimeException; 13 | import org.springframework.http.*; 14 | import org.springframework.http.client.*; 15 | import org.springframework.http.converter.StringHttpMessageConverter; 16 | import org.springframework.util.*; 17 | import org.springframework.web.client.*; 18 | 19 | import me.wulfmarius.modinstaller.ProgressListener.StepType; 20 | import me.wulfmarius.modinstaller.ProgressListeners; 21 | import me.wulfmarius.modinstaller.repository.*; 22 | import me.wulfmarius.modinstaller.utils.JsonUtils; 23 | 24 | public class RestClient { 25 | 26 | private static final RestClient INSTANCE = new RestClient(); 27 | 28 | private RestTemplate restTemplate; 29 | private Instant rateLimitReset; 30 | 31 | private RestClient() { 32 | super(); 33 | 34 | SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); 35 | requestFactory.setConnectTimeout(10000); 36 | requestFactory.setReadTimeout(10000); 37 | 38 | this.restTemplate = new RestTemplate(requestFactory); 39 | this.restTemplate.setMessageConverters(Arrays.asList(new StringHttpMessageConverter())); 40 | } 41 | 42 | public static RestClient getInstance() { 43 | return INSTANCE; 44 | } 45 | 46 | public T deserialize(ResponseEntity response, Class type, Supplier unmodifiedSupplier) { 47 | if (HttpStatus.NOT_MODIFIED.equals(response.getStatusCode())) { 48 | return unmodifiedSupplier.get(); 49 | } 50 | 51 | if (HttpStatus.NOT_FOUND.equals(response.getStatusCode())) { 52 | return null; 53 | } 54 | 55 | try { 56 | String json = response.getBody(); 57 | if (json.startsWith("\uFEFF")) { 58 | json = json.substring(1); 59 | } 60 | 61 | if (json.startsWith("\u00EF\u00BB\u00BF")) { 62 | json = json.substring(3); 63 | } 64 | 65 | return JsonUtils.deserialize(json, type); 66 | } catch (IOException e) { 67 | throw new SourceException("Could not deserialize: " + e.getMessage(), e); 68 | } 69 | } 70 | 71 | public void downloadAsset(String url, Path assetPath, ProgressListeners progressListeners) { 72 | progressListeners.stepStarted(url, StepType.DOWNLOAD); 73 | 74 | String redirectURL = url; 75 | while (redirectURL != null) { 76 | redirectURL = this.restTemplate.execute(redirectURL, HttpMethod.GET, this::prepareRequest, 77 | new DownloadResponseExtractor(assetPath, progressListeners)); 78 | } 79 | } 80 | 81 | public ResponseEntity fetch(String url, String etag) { 82 | if (this.isRateLimitReached()) { 83 | throw new RateLimitException(this.rateLimitReset); 84 | } 85 | 86 | try { 87 | ResponseEntity responseEntity = this.restTemplate.execute(url, HttpMethod.GET, new GZipRequestCallback(etag), 88 | new GzipResponseExtractor()); 89 | 90 | this.handleRateLimit(responseEntity.getHeaders()); 91 | return responseEntity; 92 | } catch (HttpClientErrorException e) { 93 | this.handleRateLimit(e.getResponseHeaders()); 94 | return ResponseEntity.status(e.getRawStatusCode()).headers(e.getResponseHeaders()).body(e.getStatusText()); 95 | } catch (NestedRuntimeException e) { 96 | Throwable mostSpecificCause = e.getMostSpecificCause(); 97 | if (mostSpecificCause instanceof UnknownHostException) { 98 | throw new HostUnreachableException(mostSpecificCause.getMessage()); 99 | } 100 | throw new RestClientException("Could not fetch from " + url + ": " + mostSpecificCause.getMessage(), mostSpecificCause); 101 | } catch (Exception e) { 102 | throw new RestClientException("Could not fetch from " + url + ": " + e.getMessage(), e); 103 | } 104 | } 105 | 106 | private void handleRateLimit(HttpHeaders headers) { 107 | String remaining = headers.getFirst("X-RateLimit-Remaining"); 108 | if (StringUtils.isEmpty(remaining) || !remaining.equals("0")) { 109 | return; 110 | } 111 | 112 | String reset = headers.getFirst("X-RateLimit-Reset"); 113 | if (StringUtils.isEmpty(reset)) { 114 | return; 115 | } 116 | 117 | this.rateLimitReset = Instant.ofEpochSecond(Long.parseLong(reset)); 118 | } 119 | 120 | private boolean isRateLimitReached() { 121 | if (this.rateLimitReset == null) { 122 | return false; 123 | } 124 | 125 | if (this.rateLimitReset.isBefore(Instant.now())) { 126 | this.rateLimitReset = null; 127 | return false; 128 | } 129 | 130 | return true; 131 | } 132 | 133 | private void prepareRequest(@SuppressWarnings("unused") ClientHttpRequest request) { 134 | // nothing to do 135 | } 136 | 137 | protected static class GZipRequestCallback implements RequestCallback { 138 | 139 | private final String etag; 140 | 141 | public GZipRequestCallback(String etag) { 142 | super(); 143 | this.etag = etag; 144 | } 145 | 146 | @Override 147 | public void doWithRequest(ClientHttpRequest request) throws IOException { 148 | request.getHeaders().setIfNoneMatch(this.etag); 149 | request.getHeaders().add("Accept-Encoding", "application/gzip"); 150 | } 151 | } 152 | 153 | protected static class GzipResponseExtractor implements ResponseExtractor> { 154 | 155 | private static Charset getCharset(ClientHttpResponse response) { 156 | try { 157 | String charsetName = response.getHeaders().getContentType().getParameter("charset"); 158 | if (charsetName != null) { 159 | return Charset.forName(charsetName); 160 | } 161 | } catch (Exception e) { 162 | // ignore 163 | } 164 | 165 | return StandardCharsets.ISO_8859_1; 166 | } 167 | 168 | @Override 169 | public ResponseEntity extractData(ClientHttpResponse response) throws IOException { 170 | InputStream inputStream = response.getBody(); 171 | if ("gzip".equalsIgnoreCase(response.getHeaders().getFirst("Content-Encoding"))) { 172 | inputStream = new GZIPInputStream(inputStream); 173 | } 174 | 175 | Charset charset = getCharset(response); 176 | String body = StreamUtils.copyToString(inputStream, charset); 177 | 178 | return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(body); 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/rest/RestClientException.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.rest; 2 | 3 | import me.wulfmarius.modinstaller.ModInstallerException; 4 | 5 | public class RestClientException extends ModInstallerException { 6 | 7 | private static final long serialVersionUID = 1L; 8 | 9 | public RestClientException(String message) { 10 | super(message); 11 | } 12 | 13 | public RestClientException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ui/BindingsFactory.java: -------------------------------------------------------------------------------- 1 | package me.wulfmarius.modinstaller.ui; 2 | 3 | import java.util.Optional; 4 | import java.util.function.Function; 5 | 6 | import javafx.beans.binding.Bindings; 7 | import javafx.beans.property.Property; 8 | import javafx.beans.value.ObservableValue; 9 | import me.wulfmarius.modinstaller.*; 10 | 11 | public class BindingsFactory { 12 | 13 | public static ObservableValue createHasValueBinding(Property property) { 14 | return Bindings.createBooleanBinding(() -> property.getValue() != null, property); 15 | } 16 | 17 | public static ObservableValue createIsNotEmptyBinding(Property property) { 18 | return Bindings.createBooleanBinding(() -> property.getValue() != null && !property.getValue().isEmpty(), property); 19 | } 20 | 21 | public static ObservableValue createModDefinitionDescriptionBinding(Property property) { 22 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getDescription, null), property); 23 | } 24 | 25 | public static ObservableValue createModDefinitionInstallBinding(Property property, 26 | ModInstaller modInstaller) { 27 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isNoVersionInstalled, false), property); 28 | } 29 | 30 | public static ObservableValue createModDefinitionNameBinding(Property property) { 31 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getName, null), property); 32 | } 33 | 34 | public static ObservableValue createModDefinitionUninstallBinding(Property property, 35 | ModInstaller modInstaller) { 36 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isAnyVersionInstalled, false), property); 37 | } 38 | 39 | public static ObservableValue createModDefinitionUpdateBinding(Property property, 40 | ModInstaller modInstaller) { 41 | return Bindings.createBooleanBinding(() -> get(property, modInstaller::isOlderVersionInstalled, false), property); 42 | } 43 | 44 | public static ObservableValue createModDefinitionURLBinding(Property property) { 45 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getUrl, null), property); 46 | } 47 | 48 | public static ObservableValue createModDefinitionVersionBinding(Property property) { 49 | return Bindings.createStringBinding(() -> get(property, ModDefinition::getVersion, null), property); 50 | } 51 | 52 | private static V get(Property property, Function function, V defaultValue) { 53 | return Optional.ofNullable(property.getValue()).map(function).orElse(defaultValue); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ui/ChangeLogViewer.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 70 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 99 | 100 | 101 |
102 | 103 | 104 | 105 | 106 |
107 | -------------------------------------------------------------------------------- /src/main/java/me/wulfmarius/modinstaller/ui/ModDetailsPanel.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 23 | 24 | 25 | 32 | 33 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 67 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 100 | 105 | 117 | 122 | 134 |