From c2d66f015192bc502ee010dccca17d7b28d41f3a Mon Sep 17 00:00:00 2001 From: D-Cysteine <54219287+D-Cysteine@users.noreply.github.com> Date: Wed, 4 Jan 2023 04:08:50 -0700 Subject: [PATCH] v0.1.0 Everything is working, but the UI needs some improvement, especially searching. Need to fix shaped recipes. --- build.gradle.kts | 19 +- libs/NESQL-Exporter-0.0.1-sql.jar | Bin 5823 -> 0 bytes libs/NESQL-Exporter-0.1.0-sql.jar | Bin 0 -> 34865 bytes .../github/dcysteine/nesql/server/Main.java | 2 +- .../nesql/server/RootController.java | 60 ++++++ .../nesql/server/ShutdownController.java | 21 -- .../nesql/server/config/ExternalConfig.java | 39 ++++ .../server/config/WebSecurityConfig.java | 33 ++++ .../nesql/server/display/Dimension.java | 18 ++ .../dcysteine/nesql/server/display/Icon.java | 31 +++ .../server/plugin/base/FluidController.java | 87 +++++++++ .../plugin/base/FluidGroupController.java | 55 ++++++ .../server/plugin/base/ItemController.java | 85 ++++++-- .../plugin/base/ItemGroupController.java | 55 ++++++ .../server/plugin/base/RecipeController.java | 54 ++++-- .../base/display/BaseDisplayService.java | 142 ++++++++++++++ .../base/display/fluid/DisplayFluid.java | 58 ++++++ .../base/display/fluid/DisplayFluidGroup.java | 56 ++++++ .../base/display/fluid/DisplayFluidStack.java | 27 +++ .../DisplayFluidStackWithProbability.java | 33 ++++ .../plugin/base/display/item/DisplayItem.java | 73 +++++++ .../base/display/item/DisplayItemGroup.java | 101 ++++++++++ .../base/display/item/DisplayItemStack.java | 26 +++ .../item/DisplayItemStackWithProbability.java | 33 ++++ .../item/DisplayWildcardItemStack.java | 56 ++++++ .../base/display/recipe/DisplayRecipe.java | 152 +++++++++++++++ .../display/recipe/DisplayRecipeType.java | 76 ++++++++ .../server/plugin/base/specs/FluidSpecs.java | 31 +++ .../server/plugin/base/specs/ItemSpecs.java | 37 ++++ .../nesql/server/service/SearchService.java | 43 ++++ .../nesql/server/util/Constants.java | 9 + .../dcysteine/nesql/server/util/NbtUtil.java | 18 -- .../nesql/server/util/NumberUtil.java | 30 +++ .../nesql/server/util/UrlBuilder.java | 38 ++++ src/main/resources/application.properties | 22 ++- src/main/resources/static/css/main.css | 43 ++++ .../image/{MissingImage.png => missing.png} | Bin .../static/js/initialize_tooltips.js | 2 + src/main/resources/templates/fragment.html | 183 ++++++++++++++++++ src/main/resources/templates/index.html | 17 ++ src/main/resources/templates/not_found.html | 10 + .../templates/plugin/base/fluid/fluid.html | 91 +++++++++ .../templates/plugin/base/fluid/search.html | 44 +++++ .../plugin/base/fluidgroup/fluid_group.html | 47 +++++ .../resources/templates/plugin/base/item.html | 14 -- .../templates/plugin/base/item/item.html | 125 ++++++++++++ .../templates/plugin/base/item/search.html | 47 +++++ .../plugin/base/itemgroup/item_group.html | 68 +++++++ .../templates/plugin/base/recipe/recipe.html | 78 ++++++++ .../resources/templates/search_results.html | 9 + src/main/resources/templates/shutdown.html | 13 +- .../templates/shutdown_disabled.html | 10 + 52 files changed, 2322 insertions(+), 99 deletions(-) delete mode 100644 libs/NESQL-Exporter-0.0.1-sql.jar create mode 100644 libs/NESQL-Exporter-0.1.0-sql.jar create mode 100644 src/main/java/com/github/dcysteine/nesql/server/RootController.java delete mode 100644 src/main/java/com/github/dcysteine/nesql/server/ShutdownController.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/config/ExternalConfig.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/config/WebSecurityConfig.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/display/Dimension.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/display/Icon.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidController.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidGroupController.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemGroupController.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/BaseDisplayService.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluid.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidGroup.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStack.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStackWithProbability.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItem.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemGroup.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStack.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStackWithProbability.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayWildcardItemStack.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipe.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipeType.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/FluidSpecs.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/ItemSpecs.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/service/SearchService.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/util/Constants.java delete mode 100644 src/main/java/com/github/dcysteine/nesql/server/util/NbtUtil.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/util/NumberUtil.java create mode 100644 src/main/java/com/github/dcysteine/nesql/server/util/UrlBuilder.java create mode 100644 src/main/resources/static/css/main.css rename src/main/resources/static/image/{MissingImage.png => missing.png} (100%) create mode 100644 src/main/resources/static/js/initialize_tooltips.js create mode 100644 src/main/resources/templates/fragment.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/not_found.html create mode 100644 src/main/resources/templates/plugin/base/fluid/fluid.html create mode 100644 src/main/resources/templates/plugin/base/fluid/search.html create mode 100644 src/main/resources/templates/plugin/base/fluidgroup/fluid_group.html delete mode 100644 src/main/resources/templates/plugin/base/item.html create mode 100644 src/main/resources/templates/plugin/base/item/item.html create mode 100644 src/main/resources/templates/plugin/base/item/search.html create mode 100644 src/main/resources/templates/plugin/base/itemgroup/item_group.html create mode 100644 src/main/resources/templates/plugin/base/recipe/recipe.html create mode 100644 src/main/resources/templates/search_results.html create mode 100644 src/main/resources/templates/shutdown_disabled.html diff --git a/build.gradle.kts b/build.gradle.kts index 5d45b1c..80dd5de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,19 @@ -buildscript { - dependencies { - classpath("com.google.protobuf:protobuf-gradle-plugin:0.9.1") - } -} - plugins { + idea id("java") id("org.springframework.boot") version "3.0.1" id("io.spring.dependency-management") version "1.1.0" } +idea { + module { + isDownloadJavadoc = true + isDownloadSources = true + } +} + group = "com.github.dcysteine.nesql.server" -version = "0.0.1" +version = "0.1.0" repositories { mavenCentral() @@ -24,9 +26,10 @@ dependencies { compileOnly("com.google.auto.value:auto-value-annotations:1.10.1") annotationProcessor("com.google.auto.value:auto-value:1.10.1") - implementation("com.google.protobuf:protobuf-java:3.21.12") + implementation("com.google.guava:guava:31.1-jre") implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") developmentOnly("org.springframework.boot:spring-boot-devtools") diff --git a/libs/NESQL-Exporter-0.0.1-sql.jar b/libs/NESQL-Exporter-0.0.1-sql.jar deleted file mode 100644 index b580bc605762fbad0c51df6c97968ccf016dfe2b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5823 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 literal 0 HcmV?d00001 diff --git a/src/main/java/com/github/dcysteine/nesql/server/Main.java b/src/main/java/com/github/dcysteine/nesql/server/Main.java index b73bdcc..e7ff316 100644 --- a/src/main/java/com/github/dcysteine/nesql/server/Main.java +++ b/src/main/java/com/github/dcysteine/nesql/server/Main.java @@ -12,7 +12,7 @@ @EntityScan(basePackageClasses = Sql.class) @EnableJpaRepositories(basePackageClasses = Sql.class) public class Main { - public static final Logger LOGGER = LoggerFactory.getLogger("NESQL Server"); + public static final Logger Logger = LoggerFactory.getLogger("NESQL Server"); public static void main(String[] args) { SpringApplication.run(Main.class, args); diff --git a/src/main/java/com/github/dcysteine/nesql/server/RootController.java b/src/main/java/com/github/dcysteine/nesql/server/RootController.java new file mode 100644 index 0000000..17934fc --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/RootController.java @@ -0,0 +1,60 @@ +package com.github.dcysteine.nesql.server; + +import com.github.dcysteine.nesql.server.config.ExternalConfig; +import com.github.dcysteine.nesql.server.util.NumberUtil; +import com.github.dcysteine.nesql.sql.base.fluid.FluidRepository; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +/** Serves root-level endpoints. */ +@Controller +public class RootController { + @Autowired + ExternalConfig externalConfig; + + @Autowired + private ApplicationContext context; + + @Autowired + private ItemRepository itemRepository; + + @Autowired + private FluidRepository fluidRepository; + + @Autowired + private RecipeRepository recipeRepository; + + @GetMapping(value = {"/", "/index"}) + public String index(Model model) { + model.addAttribute( + "itemCount", NumberUtil.formatInteger(itemRepository.count())); + model.addAttribute( + "fluidCount", NumberUtil.formatInteger(fluidRepository.count())); + model.addAttribute( + "recipeCount", NumberUtil.formatInteger(recipeRepository.count())); + return "index"; + } + + @GetMapping("/notfound") + public String notFound() { + return "not_found"; + } + + @GetMapping("/shutdown") + public String shutDown() { + if (!externalConfig.isShutdownEnabled()) { + return "shutdown_disabled"; + } + + // We need to actually shut down in a separate thread, so that we can proceed with serving + // the shutdown page. + new Thread(() -> SpringApplication.exit(context)).start(); + return "shutdown"; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/ShutdownController.java b/src/main/java/com/github/dcysteine/nesql/server/ShutdownController.java deleted file mode 100644 index 4aef619..0000000 --- a/src/main/java/com/github/dcysteine/nesql/server/ShutdownController.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.dcysteine.nesql.server; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class ShutdownController { - @Autowired - private ApplicationContext context; - - @GetMapping("/shutdown") - public String shutDown() { - // We need to actually shut down in a separate thread, so that we can proceed with serving - // the shutdown page. - new Thread(() -> SpringApplication.exit(context)).start(); - return "shutdown"; - } -} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/config/ExternalConfig.java b/src/main/java/com/github/dcysteine/nesql/server/config/ExternalConfig.java new file mode 100644 index 0000000..72d33d0 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/config/ExternalConfig.java @@ -0,0 +1,39 @@ +package com.github.dcysteine.nesql.server.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * Class holding config values that are meant to be externally configurable via the + * {@code application.properties} file. + */ +@Component +public class ExternalConfig { + @Value("${nesql.server.repository-name}") + private String repositoryName; + + @Value("${nesql.server.page-size}") + private int pageSize; + + @Value("${nesql.server.enable-shutdown}") + private boolean shutdownEnabled; + + @Value("${nesql.server.allow-external-users}") + private boolean externalUsersAllowed; + + public String getRepositoryName() { + return repositoryName; + } + + public int getPageSize() { + return pageSize; + } + + public boolean isShutdownEnabled() { + return shutdownEnabled; + } + + public boolean areExternalUsersAllowed() { + return externalUsersAllowed; + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/config/WebSecurityConfig.java b/src/main/java/com/github/dcysteine/nesql/server/config/WebSecurityConfig.java new file mode 100644 index 0000000..9129a54 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/config/WebSecurityConfig.java @@ -0,0 +1,33 @@ +package com.github.dcysteine.nesql.server.config; + +import com.google.common.base.Joiner; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class WebSecurityConfig { + @Autowired + ExternalConfig externalConfig; + + private static final String CONTENT_SECURITY_POLICY = Joiner.on("; ").join( + "default-src 'self'", + // This is the hash of our image onerror handler: + // this.src='/image/missing.png';this.onerror=''; + "script-src 'self' 'sha256-KrWF/SaFRimyTg5FZWV3gqZlxM6hGYkkM1k3ePvN71w=' 'unsafe-hashes'"); + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + // TODO restrict requests to same-IP only? At least by default + http.headers().contentSecurityPolicy(CONTENT_SECURITY_POLICY); + + if (!externalConfig.areExternalUsersAllowed()) { + http.authorizeRequests().anyRequest() + .access("hasIpAddress('127.0.0.1') or hasIpAddress('::1')"); + } + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/display/Dimension.java b/src/main/java/com/github/dcysteine/nesql/server/display/Dimension.java new file mode 100644 index 0000000..7300f0c --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/display/Dimension.java @@ -0,0 +1,18 @@ +package com.github.dcysteine.nesql.server.display; + +import com.google.auto.value.AutoValue; + +/** Immutable class representing a width and height. */ +@AutoValue +public abstract class Dimension { + public static Dimension create(int width, int height) { + return new AutoValue_Dimension(width, height); + } + + public static Dimension create(int width) { + return new AutoValue_Dimension(width, width); + } + + public abstract int getWidth(); + public abstract int getHeight(); +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/display/Icon.java b/src/main/java/com/github/dcysteine/nesql/server/display/Icon.java new file mode 100644 index 0000000..4dffd2c --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/display/Icon.java @@ -0,0 +1,31 @@ +package com.github.dcysteine.nesql.server.display; + +import com.google.auto.value.AutoValue; + +import javax.annotation.Nullable; + +@AutoValue +public abstract class Icon { + public abstract String getDescription(); + public abstract String getUrl(); + public abstract String getImageFilePath(); + @Nullable public abstract String getTopLeft(); + @Nullable public abstract String getBottomRight(); + + public static Builder builder() { + return new AutoValue_Icon.Builder(); + } + + public abstract Builder toBuilder(); + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setDescription(String description); + public abstract Builder setUrl(String url); + public abstract Builder setImageFilePath(String imageFilePath); + public abstract Builder setTopLeft(@Nullable String topLeft); + public abstract Builder setBottomRight(@Nullable String bottomRight); + + public abstract Icon build(); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidController.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidController.java new file mode 100644 index 0000000..87caa0f --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidController.java @@ -0,0 +1,87 @@ +package com.github.dcysteine.nesql.server.plugin.base; + +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluid; +import com.github.dcysteine.nesql.server.plugin.base.specs.FluidSpecs; +import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayService; +import com.github.dcysteine.nesql.server.service.SearchService; +import com.github.dcysteine.nesql.sql.base.fluid.Fluid; +import com.github.dcysteine.nesql.sql.base.fluid.FluidRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.function.Predicate; + +@Controller +@RequestMapping(path = "/fluid") +public class FluidController { + @Autowired + private FluidRepository fluidRepository; + + @Autowired + private BaseDisplayService baseDisplayService; + + @Autowired + private SearchService searchService; + + @GetMapping(path = "/{fluid_id}") + public String view(@PathVariable(name = "fluid_id") String id, Model model) { + Optional fluidOptional = fluidRepository.findById(id); + if (fluidOptional.isEmpty()) { + return "not_found"; + } + Fluid fluid = fluidOptional.get(); + DisplayFluid displayFluid = baseDisplayService.buildDisplayFluid(fluid); + + model.addAttribute("fluid", fluid); + model.addAttribute("displayFluid", displayFluid); + return "plugin/base/fluid/fluid"; + } + + @GetMapping(path = "/search") + public String search() { + return "plugin/base/fluid/search"; + } + + @GetMapping(path = "/all") + public String all(@RequestParam(defaultValue = "1") int page, Model model) { + return searchService.handleGetAll( + page, model, fluidRepository, baseDisplayService::buildDisplayFluid); + } + + @GetMapping(path = "/searchresults") + public String searchResults( + @RequestParam(required = false) Optional localizedName, + @RequestParam(required = false) Optional internalName, + @RequestParam(required = false) Optional fluidId, + @RequestParam(defaultValue = "1") int page, + Model model) { + @Nullable + Specification localizedNameSpec = + localizedName + .filter(Predicate.not(String::isEmpty)) + .map(FluidSpecs::buildLocalizedNameSpec).orElse(null); + + @Nullable + Specification internalNameSpec = + internalName + .filter(Predicate.not(String::isEmpty)) + .map(FluidSpecs::buildInternalNameSpec).orElse(null); + + @Nullable + Specification fluidIdSpec = + fluidId.map(FluidSpecs::buildFluidIdSpec).orElse(null); + + Specification spec = + Specification.allOf(localizedNameSpec, internalNameSpec, fluidIdSpec); + return searchService.handleSearch( + page, model, fluidRepository, spec, baseDisplayService::buildDisplayFluidIcon); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidGroupController.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidGroupController.java new file mode 100644 index 0000000..1706d11 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/FluidGroupController.java @@ -0,0 +1,55 @@ +package com.github.dcysteine.nesql.server.plugin.base; + +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayService; +import com.github.dcysteine.nesql.server.service.SearchService; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroup; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroupRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Optional; + +@Controller +@RequestMapping(path = "/fluidgroup") +public class FluidGroupController { + @Autowired + private FluidGroupRepository fluidGroupRepository; + + @Autowired + private BaseDisplayService baseDisplayService; + + @Autowired + private SearchService searchService; + + @GetMapping(path = "/{fluid_group_id}") + public String view(@PathVariable(name = "fluid_group_id") String id, Model model) { + Optional fluidGroupOptional = fluidGroupRepository.findById(id); + if (fluidGroupOptional.isEmpty()) { + return "not_found"; + } + FluidGroup fluidGroup = fluidGroupOptional.get(); + DisplayFluidGroup displayFluidGroup = baseDisplayService.buildDisplayFluidGroup(fluidGroup); + + model.addAttribute("fluidGroup", fluidGroup); + model.addAttribute("displayFluidGroup", displayFluidGroup); + return "plugin/base/fluidgroup/fluid_group"; + } + + @GetMapping(path = "/search") + public String search() { + // TODO add a search page + return "redirect:all"; + } + + @GetMapping(path = "/all") + public String all(@RequestParam(defaultValue = "1") int page, Model model) { + return searchService.handleGetAll( + page, model, fluidGroupRepository, baseDisplayService::buildDisplayFluidGroupIcon); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemController.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemController.java index 96ffaf1..7ac2825 100644 --- a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemController.java +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemController.java @@ -1,33 +1,94 @@ package com.github.dcysteine.nesql.server.plugin.base; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItem; +import com.github.dcysteine.nesql.server.plugin.base.specs.ItemSpecs; +import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayService; +import com.github.dcysteine.nesql.server.service.SearchService; import com.github.dcysteine.nesql.sql.base.item.Item; import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.google.common.base.Strings; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.function.Predicate; + @Controller @RequestMapping(path = "/item") public class ItemController { @Autowired private ItemRepository itemRepository; - @GetMapping - public String item(@RequestParam(name = "id") String id, Model model) { - Item item = itemRepository.findById(id).get(); - model.addAttribute("id", id); - model.addAttribute("imageFilePath", item.getImageFilePath()); - model.addAttribute("name", item.getInternalName()); - return "plugin/base/item"; + @Autowired + private BaseDisplayService baseDisplayService; + + @Autowired + private SearchService searchService; + + @GetMapping(path = "/{item_id}") + public String view(@PathVariable(name = "item_id") String id, Model model) { + Optional itemOptional = itemRepository.findById(id); + if (itemOptional.isEmpty()) { + return "not_found"; + } + Item item = itemOptional.get(); + DisplayItem displayItem = baseDisplayService.buildDisplayItem(item); + + model.addAttribute("item", item); + model.addAttribute("displayItem", displayItem); + return "plugin/base/item/item"; + } + + @GetMapping(path = "/search") + public String search() { + return "plugin/base/item/search"; + } + + @GetMapping(path = "/all") + public String all(@RequestParam(defaultValue = "1") int page, Model model) { + return searchService.handleGetAll( + page, model, itemRepository, baseDisplayService::buildDisplayItem); } - @GetMapping(path="/test") - public String test(Model model) { - model.addAttribute("id", "id"); - model.addAttribute("name", "name"); - return "plugin/base/item"; + @GetMapping(path = "/searchresults") + public String searchResults( + @RequestParam(required = false) Optional localizedName, + @RequestParam(required = false) Optional internalName, + @RequestParam(required = false) Optional itemId, + @RequestParam(required = false) Optional itemDamage, + @RequestParam(defaultValue = "1") int page, + Model model) { + @Nullable + Specification localizedNameSpec = + localizedName + .filter(Predicate.not(String::isEmpty)) + .map(ItemSpecs::buildLocalizedNameSpec).orElse(null); + + @Nullable + Specification internalNameSpec = + internalName + .filter(Predicate.not(String::isEmpty)) + .map(ItemSpecs::buildInternalNameSpec).orElse(null); + + @Nullable + Specification itemIdSpec = + itemId.map(ItemSpecs::buildItemIdSpec).orElse(null); + + @Nullable + Specification itemDamageSpec = + itemDamage.map(ItemSpecs::buildItemDamageSpec).orElse(null); + + Specification spec = + Specification.allOf( + localizedNameSpec, internalNameSpec, itemIdSpec, itemDamageSpec); + return searchService.handleSearch( + page, model, itemRepository, spec, baseDisplayService::buildDisplayItemIcon); } } diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemGroupController.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemGroupController.java new file mode 100644 index 0000000..28585c0 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/ItemGroupController.java @@ -0,0 +1,55 @@ +package com.github.dcysteine.nesql.server.plugin.base; + +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayService; +import com.github.dcysteine.nesql.server.service.SearchService; +import com.github.dcysteine.nesql.sql.base.item.ItemGroup; +import com.github.dcysteine.nesql.sql.base.item.ItemGroupRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Optional; + +@Controller +@RequestMapping(path = "/itemgroup") +public class ItemGroupController { + @Autowired + private ItemGroupRepository itemGroupRepository; + + @Autowired + private BaseDisplayService baseDisplayService; + + @Autowired + private SearchService searchService; + + @GetMapping(path = "/{item_group_id}") + public String view(@PathVariable(name = "item_group_id") String id, Model model) { + Optional itemGroupOptional = itemGroupRepository.findById(id); + if (itemGroupOptional.isEmpty()) { + return "not_found"; + } + ItemGroup itemGroup = itemGroupOptional.get(); + DisplayItemGroup displayItemGroup = baseDisplayService.buildDisplayItemGroup(itemGroup); + + model.addAttribute("itemGroup", itemGroup); + model.addAttribute("displayItemGroup", displayItemGroup); + return "plugin/base/itemgroup/item_group"; + } + + @GetMapping(path = "/search") + public String search() { + // TODO add a search page + return "redirect:all"; + } + + @GetMapping(path = "/all") + public String all(@RequestParam(defaultValue = "1") int page, Model model) { + return searchService.handleGetAll( + page, model, itemGroupRepository, baseDisplayService::buildDisplayItemGroupIcon); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/RecipeController.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/RecipeController.java index 7b9738f..35467e3 100644 --- a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/RecipeController.java +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/RecipeController.java @@ -1,33 +1,55 @@ package com.github.dcysteine.nesql.server.plugin.base; -import com.github.dcysteine.nesql.sql.base.item.Item; -import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayService; +import com.github.dcysteine.nesql.server.service.SearchService; +import com.github.dcysteine.nesql.sql.base.recipe.Recipe; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import java.util.Optional; + @Controller @RequestMapping(path = "/recipe") public class RecipeController { @Autowired - private ItemRepository itemRepository; - - @GetMapping - public String item(@RequestParam(name = "id") String id, Model model) { - Item item = itemRepository.findById(id).get(); - model.addAttribute("id", id); - model.addAttribute("imageFilePath", item.getImageFilePath()); - model.addAttribute("name", item.getInternalName()); - return "base/item"; + private RecipeRepository recipeRepository; + + @Autowired + private BaseDisplayService baseDisplayService; + + @Autowired + private SearchService searchService; + + @GetMapping(path = "/{recipe_id}") + public String view(@PathVariable(name = "recipe_id") String id, Model model) { + Optional recipeOptional = recipeRepository.findById(id); + if (recipeOptional.isEmpty()) { + return "not_found"; + } + Recipe recipe = recipeOptional.get(); + DisplayRecipe displayRecipe = baseDisplayService.buildDisplayRecipe(recipe); + + model.addAttribute("recipe", recipe); + model.addAttribute("displayRecipe", displayRecipe); + return "plugin/base/recipe/recipe"; + } + + @GetMapping(path = "/search") + public String search() { + // TODO add a search page + return "redirect:all"; } - @GetMapping(path="/test") - public String test(Model model) { - model.addAttribute("id", "id"); - model.addAttribute("name", "name"); - return "base/item"; + @GetMapping(path = "/all") + public String all(@RequestParam(defaultValue = "1") int page, Model model) { + return searchService.handleGetAll( + page, model, recipeRepository, baseDisplayService::buildDisplayRecipeIcon); } } diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/BaseDisplayService.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/BaseDisplayService.java new file mode 100644 index 0000000..73ea59e --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/BaseDisplayService.java @@ -0,0 +1,142 @@ +package com.github.dcysteine.nesql.server.plugin.base.display; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluid; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidStack; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidStackWithProbability; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItem; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemStack; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemStackWithProbability; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayWildcardItemStack; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipeType; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroup; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroupRepository; +import com.github.dcysteine.nesql.sql.base.fluid.Fluid; +import com.github.dcysteine.nesql.sql.base.fluid.FluidRepository; +import com.github.dcysteine.nesql.sql.base.fluid.FluidStack; +import com.github.dcysteine.nesql.sql.base.fluid.FluidStackWithProbability; +import com.github.dcysteine.nesql.sql.base.item.ItemGroup; +import com.github.dcysteine.nesql.sql.base.item.ItemGroupRepository; +import com.github.dcysteine.nesql.sql.base.item.Item; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.item.ItemStack; +import com.github.dcysteine.nesql.sql.base.item.ItemStackWithProbability; +import com.github.dcysteine.nesql.sql.base.item.WildcardItemStack; +import com.github.dcysteine.nesql.sql.base.recipe.Recipe; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeType; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +// TODO I should probably refactor this... somehow +/** Makes life easier for us by taking care of all required dependencies for display objects. */ +@Service +public class BaseDisplayService { + @Autowired + private ItemRepository itemRepository; + + @Autowired + private ItemGroupRepository itemGroupRepository; + + @Autowired + private FluidRepository fluidRepository; + + @Autowired + private FluidGroupRepository fluidGroupRepository; + + @Autowired + private RecipeRepository recipeRepository; + + public DisplayItem buildDisplayItem(Item item) { + return DisplayItem.create(item, itemRepository, itemGroupRepository, recipeRepository); + } + + public Icon buildDisplayItemIcon(Item item) { + return DisplayItem.buildIcon(item); + } + + public DisplayItemStack buildDisplayItemStack(ItemStack itemStack) { + return DisplayItemStack.create(itemStack); + } + + public Icon buildDisplayItemStackIcon(ItemStack itemStack) { + return DisplayItemStack.buildIcon(itemStack); + } + + public DisplayItemStackWithProbability buildDisplayItemStackWithProbability( + ItemStackWithProbability itemStack) { + return DisplayItemStackWithProbability.create(itemStack); + } + + public Icon buildDisplayItemStackWithProbabilityIcon( + ItemStackWithProbability itemStack) { + return DisplayItemStackWithProbability.buildIcon(itemStack); + } + + public DisplayWildcardItemStack buildDisplayWildcardItemStack( + WildcardItemStack wildcardItemStack) { + return DisplayWildcardItemStack.create(wildcardItemStack, itemRepository); + } + + public Icon buildDisplayWildcardItemStackIcon( + WildcardItemStack wildcardItemStack) { + return DisplayWildcardItemStack.buildIcon(wildcardItemStack, itemRepository); + } + + public DisplayItemGroup buildDisplayItemGroup(ItemGroup itemGroup) { + return DisplayItemGroup.create(itemGroup, itemRepository); + } + + public Icon buildDisplayItemGroupIcon(ItemGroup itemGroup) { + return DisplayItemGroup.buildIcon(itemGroup, itemRepository); + } + + public DisplayFluid buildDisplayFluid(Fluid fluid) { + return DisplayFluid.create(fluid, fluidGroupRepository, recipeRepository); + } + + public Icon buildDisplayFluidIcon(Fluid fluid) { + return DisplayFluid.buildIcon(fluid); + } + + public DisplayFluidStack buildDisplayFluidStack(FluidStack fluidStack) { + return DisplayFluidStack.create(fluidStack); + } + + public Icon buildDisplayFluidStackIcon(FluidStack fluidStack) { + return DisplayFluidStack.buildIcon(fluidStack); + } + + public DisplayFluidStackWithProbability buildDisplayFluidStackWithProbability( + FluidStackWithProbability fluidStack) { + return DisplayFluidStackWithProbability.create(fluidStack); + } + + public Icon buildDisplayFluidStackWithProbabilityIcon( + FluidStackWithProbability fluidStack) { + return DisplayFluidStackWithProbability.buildIcon(fluidStack); + } + + public DisplayFluidGroup buildDisplayFluidGroup(FluidGroup fluidGroup) { + return DisplayFluidGroup.create(fluidGroup); + } + + public Icon buildDisplayFluidGroupIcon(FluidGroup fluidGroup) { + return DisplayFluidGroup.buildIcon(fluidGroup); + } + + public DisplayRecipe buildDisplayRecipe(Recipe recipe) { + return DisplayRecipe.create(recipe, itemRepository); + } + + public Icon buildDisplayRecipeIcon(Recipe recipe) { + return DisplayRecipe.buildIcon(recipe); + } + + public DisplayRecipeType buildDisplayRecipeType(RecipeType recipe) { + return DisplayRecipeType.create(recipe); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluid.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluid.java new file mode 100644 index 0000000..33aedb8 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluid.java @@ -0,0 +1,58 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.fluid; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.fluid.Fluid; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroupRepository; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +@AutoValue +public abstract class DisplayFluid implements Comparable { + public static DisplayFluid create( + Fluid fluid, + FluidGroupRepository fluidGroupRepository, + RecipeRepository recipeRepository) { + ImmutableList recipesWithInput = + recipeRepository.findByFluidInput(fluid.getId()).stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableList recipesWithOutput = + recipeRepository.findByFluidInput(fluid.getId()).stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + + ImmutableList fluidGroupsContaining = + fluidGroupRepository.findByFluid(fluid.getId()).stream() + .sorted() + .map(DisplayFluidGroup::buildIcon) + .collect(ImmutableList.toImmutableList()); + + return new AutoValue_DisplayFluid( + fluid, buildIcon(fluid), + recipesWithInput, recipesWithOutput, fluidGroupsContaining); + } + + public static Icon buildIcon(Fluid fluid) { + return Icon.builder() + .setDescription(fluid.getLocalizedName()) + .setUrl(UrlBuilder.buildFluidUrl(fluid)) + .setImageFilePath(fluid.getImageFilePath()) + .build(); + } + + public abstract Fluid getFluid(); + public abstract Icon getIcon(); + public abstract ImmutableList getFluidGroupsContaining(); + public abstract ImmutableList getRecipesWithInput(); + public abstract ImmutableList getRecipesWithOutput(); + + @Override + public int compareTo(DisplayFluid other) { + return getFluid().compareTo(other.getFluid()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidGroup.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidGroup.java new file mode 100644 index 0000000..feebe36 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidGroup.java @@ -0,0 +1,56 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.fluid; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.util.Constants; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroup; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +@AutoValue +public abstract class DisplayFluidGroup implements Comparable { + public static DisplayFluidGroup create(FluidGroup fluidGroup) { + ImmutableList recipesWithInput = + fluidGroup.getRecipesWithInput().stream() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + + ImmutableList fluidStacks = + fluidGroup.getFluidStacks().stream() + .map(DisplayFluidStack::buildIcon) + .collect(ImmutableList.toImmutableList()); + + return new AutoValue_DisplayFluidGroup( + fluidGroup, buildIcon(fluidGroup), recipesWithInput, fluidStacks); + } + + public static Icon buildIcon(FluidGroup fluidGroup) { + String url = UrlBuilder.buildFluidGroupUrl(fluidGroup); + Icon icon; + if (!fluidGroup.getFluidStacks().isEmpty()) { + int size = fluidGroup.getFluidStacks().size(); + icon = DisplayFluidStack.buildIcon(fluidGroup.getFluidStacks().first()).toBuilder() + .setDescription(String.format("Fluid Group (%d fluid stacks)", size)) + .setUrl(url) + .build(); + } else { + icon = Icon.builder() + .setDescription("Fluid Group (empty)") + .setUrl(url) + .setImageFilePath(Constants.MISSING_IMAGE) + .build(); + } + return icon; + } + + public abstract FluidGroup getFluidGroup(); + public abstract Icon getIcon(); + public abstract ImmutableList getRecipesWithInput(); + public abstract ImmutableList getFluidStacks(); + + @Override + public int compareTo(DisplayFluidGroup other) { + return getFluidGroup().compareTo(other.getFluidGroup()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStack.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStack.java new file mode 100644 index 0000000..49fe98e --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStack.java @@ -0,0 +1,27 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.fluid; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.util.NumberUtil; +import com.github.dcysteine.nesql.sql.base.fluid.FluidStack; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class DisplayFluidStack implements Comparable { + public static DisplayFluidStack create(FluidStack fluidStack) { + return new AutoValue_DisplayFluidStack(fluidStack, buildIcon(fluidStack)); + } + + public static Icon buildIcon(FluidStack fluidStack) { + return DisplayFluid.buildIcon(fluidStack.getFluid()).toBuilder() + .setBottomRight(NumberUtil.formatCompact(fluidStack.getAmount())) + .build(); + } + + public abstract FluidStack getFluidStack(); + public abstract Icon getIcon(); + + @Override + public int compareTo(DisplayFluidStack other) { + return getFluidStack().compareTo(other.getFluidStack()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStackWithProbability.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStackWithProbability.java new file mode 100644 index 0000000..ae00ce1 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/fluid/DisplayFluidStackWithProbability.java @@ -0,0 +1,33 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.fluid; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.util.NumberUtil; +import com.github.dcysteine.nesql.sql.base.fluid.FluidStackWithProbability; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class DisplayFluidStackWithProbability + implements Comparable { + public static DisplayFluidStackWithProbability create(FluidStackWithProbability fluidStack) { + return new AutoValue_DisplayFluidStackWithProbability(fluidStack, buildIcon(fluidStack)); + } + + public static Icon buildIcon(FluidStackWithProbability fluidStack) { + Icon icon = DisplayFluidStack.buildIcon(fluidStack.withoutProbability()); + if (NumberUtil.fuzzyEquals(fluidStack.getProbability(), 1.0d)) { + return icon; + } else { + return icon.toBuilder() + .setTopLeft(NumberUtil.formatPercentage(fluidStack.getProbability())) + .build(); + } + } + + public abstract FluidStackWithProbability getFluidStack(); + public abstract Icon getIcon(); + + @Override + public int compareTo(DisplayFluidStackWithProbability other) { + return getFluidStack().compareTo(other.getFluidStack()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItem.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItem.java new file mode 100644 index 0000000..c3b8548 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItem.java @@ -0,0 +1,73 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.item; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.item.Item; +import com.github.dcysteine.nesql.sql.base.item.ItemGroupRepository; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +@AutoValue +public abstract class DisplayItem implements Comparable { + public static DisplayItem create( + Item item, + ItemRepository itemRepository, + ItemGroupRepository itemGroupRepository, + RecipeRepository recipeRepository) { + ImmutableList recipesWithInput = + recipeRepository.findByItemInput(item.getId()).stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableList recipesWithWildcardInput = + recipeRepository.findByWildcardItemInput(item.getItemId()).stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableList recipesWithOutput = + recipeRepository.findByItemOutput(item.getId()).stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + + ImmutableList itemGroupsContaining = + itemGroupRepository.findByItem(item.getId()).stream() + .sorted() + .map(itemGroup -> DisplayItemGroup.buildIcon(itemGroup, itemRepository)) + .collect(ImmutableList.toImmutableList()); + ImmutableList itemGroupsContainingWildcard = + itemGroupRepository.findByWildcardItemId(item.getItemId()).stream() + .sorted() + .map(itemGroup -> DisplayItemGroup.buildIcon(itemGroup, itemRepository)) + .collect(ImmutableList.toImmutableList()); + + return new AutoValue_DisplayItem( + item, buildIcon(item), + recipesWithInput, recipesWithWildcardInput, recipesWithOutput, + itemGroupsContaining, itemGroupsContainingWildcard); + } + + public static Icon buildIcon(Item item) { + return Icon.builder() + .setDescription(item.getLocalizedName()) + .setUrl(UrlBuilder.buildItemUrl(item)) + .setImageFilePath(item.getImageFilePath()) + .build(); + } + + public abstract Item getItem(); + public abstract Icon getIcon(); + public abstract ImmutableList getRecipesWithInput(); + public abstract ImmutableList getRecipesWithWildcardInput(); + public abstract ImmutableList getRecipesWithOutput(); + public abstract ImmutableList getItemGroupsContaining(); + public abstract ImmutableList getItemGroupsContainingWildcard(); + + @Override + public int compareTo(DisplayItem other) { + return getItem().compareTo(other.getItem()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemGroup.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemGroup.java new file mode 100644 index 0000000..f1c39e7 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemGroup.java @@ -0,0 +1,101 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.item; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.recipe.DisplayRecipe; +import com.github.dcysteine.nesql.server.util.Constants; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.item.ItemGroup; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.item.ItemStack; +import com.github.dcysteine.nesql.sql.base.item.WildcardItemStack; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; + +import java.util.SortedSet; + +@AutoValue +public abstract class DisplayItemGroup implements Comparable { + public static DisplayItemGroup create(ItemGroup itemGroup, ItemRepository itemRepository) { + ImmutableList recipesWithInput = + itemGroup.getRecipesWithInput().stream() + .sorted() + .map(DisplayRecipe::buildIcon) + .collect(ImmutableList.toImmutableList()); + + ImmutableList itemStacks = + itemGroup.getItemStacks().stream() + .map(DisplayItemStack::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableList wildcardItemStacks = + itemGroup.getWildcardItemStacks().stream() + .map(wildcardItemStack -> + DisplayWildcardItemStack.buildIcon( + wildcardItemStack, itemRepository)) + .collect(ImmutableList.toImmutableList()); + + ImmutableListMultimap.Builder builder = + ImmutableListMultimap.builder(); + for (WildcardItemStack wildcardItemStack : itemGroup.getWildcardItemStacks()) { + int itemId = wildcardItemStack.getItemId(); + itemRepository.findByItemId(itemId).stream() + .sorted() + .map(item -> new ItemStack(item, wildcardItemStack.getStackSize())) + .map(DisplayItemStack::buildIcon) + .forEach(itemStack -> builder.put(itemId, itemStack)); + } + ImmutableListMultimap resolvedWildcardItemStacks = builder.build(); + + return new AutoValue_DisplayItemGroup( + itemGroup, buildIcon(itemGroup, itemRepository), + recipesWithInput, itemStacks, wildcardItemStacks, resolvedWildcardItemStacks); + } + + public static Icon buildIcon(ItemGroup itemGroup, ItemRepository itemRepository) { + SortedSet itemStacks = itemGroup.getItemStacks(); + SortedSet wildcardItemStacks = itemGroup.getWildcardItemStacks(); + + int size = itemStacks.size(); + for (WildcardItemStack wildcardItemStack : wildcardItemStacks) { + int itemId = wildcardItemStack.getItemId(); + size += itemRepository.findByItemId(itemId).size(); + } + + String url = UrlBuilder.buildItemGroupUrl(itemGroup); + Icon icon; + if (!itemStacks.isEmpty()) { + icon = DisplayItemStack.buildIcon(itemStacks.first()).toBuilder() + .setDescription(String.format("Item Group (%d item stacks)", size)) + .setUrl(url) + .build(); + } else if (!wildcardItemStacks.isEmpty()) { + icon = DisplayWildcardItemStack.buildIcon(wildcardItemStacks.first(), itemRepository) + .toBuilder() + .setDescription( + String.format( + "Wildcard Item Group (%d keys, %d item stacks)", + wildcardItemStacks.size(), size)) + .setUrl(url) + .build(); + } else { + icon = Icon.builder() + .setDescription("Item Group (empty)") + .setUrl(url) + .setImageFilePath(Constants.MISSING_IMAGE) + .build(); + } + return icon; + } + + public abstract ItemGroup getItemGroup(); + public abstract Icon getIcon(); + public abstract ImmutableList getRecipesWithInput(); + public abstract ImmutableList getItemStacks(); + public abstract ImmutableList getWildcardItemStacks(); + public abstract ImmutableListMultimap getResolvedWildcardItemStacks(); + + @Override + public int compareTo(DisplayItemGroup other) { + return getItemGroup().compareTo(other.getItemGroup()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStack.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStack.java new file mode 100644 index 0000000..7299c40 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStack.java @@ -0,0 +1,26 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.item; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.sql.base.item.ItemStack; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class DisplayItemStack implements Comparable { + public static DisplayItemStack create(ItemStack itemStack) { + return new AutoValue_DisplayItemStack(itemStack, buildIcon(itemStack)); + } + + public static Icon buildIcon(ItemStack itemStack) { + return DisplayItem.buildIcon(itemStack.getItem()).toBuilder() + .setBottomRight(Integer.toString(itemStack.getStackSize())) + .build(); + } + + public abstract ItemStack getItemStack(); + public abstract Icon getIcon(); + + @Override + public int compareTo(DisplayItemStack other) { + return getItemStack().compareTo(other.getItemStack()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStackWithProbability.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStackWithProbability.java new file mode 100644 index 0000000..5cf5887 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayItemStackWithProbability.java @@ -0,0 +1,33 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.item; + +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.util.NumberUtil; +import com.github.dcysteine.nesql.sql.base.item.ItemStackWithProbability; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class DisplayItemStackWithProbability + implements Comparable { + public static DisplayItemStackWithProbability create(ItemStackWithProbability itemStack) { + return new AutoValue_DisplayItemStackWithProbability(itemStack, buildIcon(itemStack)); + } + + public static Icon buildIcon(ItemStackWithProbability itemStack) { + Icon icon = DisplayItemStack.buildIcon(itemStack.withoutProbability()); + if (NumberUtil.fuzzyEquals(itemStack.getProbability(), 1.0d)) { + return icon; + } else { + return icon.toBuilder() + .setTopLeft(NumberUtil.formatPercentage(itemStack.getProbability())) + .build(); + } + } + + public abstract ItemStackWithProbability getItemStack(); + public abstract Icon getIcon(); + + @Override + public int compareTo(DisplayItemStackWithProbability other) { + return getItemStack().compareTo(other.getItemStack()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayWildcardItemStack.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayWildcardItemStack.java new file mode 100644 index 0000000..dc39b1a --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/item/DisplayWildcardItemStack.java @@ -0,0 +1,56 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.item; + +import com.github.dcysteine.nesql.server.Main; +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.item.Item; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.item.WildcardItemStack; +import com.google.auto.value.AutoValue; + +import java.util.List; + +@AutoValue +public abstract class DisplayWildcardItemStack implements Comparable { + public static DisplayWildcardItemStack create( + WildcardItemStack wildcardItemStack, ItemRepository itemRepository) { + return new AutoValue_DisplayWildcardItemStack( + wildcardItemStack, buildIcon(wildcardItemStack, itemRepository)); + } + + public static Icon buildIcon( + WildcardItemStack wildcardItemStack, ItemRepository itemRepository) { + List items = itemRepository.findBaseItemByItemId(wildcardItemStack.getItemId()); + Icon icon; + if (items.isEmpty()) { + Main.Logger.error( + "Could not find base item for item id: {}", wildcardItemStack.getItemId()); + + icon = Icon.builder() + .setDescription( + String.format( + "Wildcard Item Stack (%d)", wildcardItemStack.getItemId())) + .setUrl(UrlBuilder.getMissingUrl()) + .setTopLeft(Integer.toString(wildcardItemStack.getItemId())) + .setBottomRight(Integer.toString(wildcardItemStack.getStackSize())) + .build(); + } else { + Icon itemIcon = DisplayItem.buildIcon(items.get(0)); + icon = itemIcon.toBuilder() + .setDescription( + String.format("Wildcard Item Stack (%s)", itemIcon.getDescription())) + .setTopLeft(Integer.toString(wildcardItemStack.getItemId())) + .setBottomRight(Integer.toString(wildcardItemStack.getStackSize())) + .build(); + } + return icon; + } + + public abstract WildcardItemStack getWildcardItemStack(); + public abstract Icon getIcon(); + + @Override + public int compareTo(DisplayWildcardItemStack other) { + return getWildcardItemStack().compareTo(other.getWildcardItemStack()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipe.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipe.java new file mode 100644 index 0000000..caafbaa --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipe.java @@ -0,0 +1,152 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.recipe; + +import com.github.dcysteine.nesql.server.display.Dimension; +import com.github.dcysteine.nesql.server.display.Icon; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.fluid.DisplayFluidStackWithProbability; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemGroup; +import com.github.dcysteine.nesql.server.plugin.base.display.item.DisplayItemStackWithProbability; +import com.github.dcysteine.nesql.server.util.Constants; +import com.github.dcysteine.nesql.server.Main; +import com.github.dcysteine.nesql.server.util.UrlBuilder; +import com.github.dcysteine.nesql.sql.base.item.ItemRepository; +import com.github.dcysteine.nesql.sql.base.recipe.Recipe; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableTable; + +import java.util.Iterator; + +/** + * Packages up a {@link Recipe} for displaying on a page. + * + *

Inputs and outputs are returned as {@link ImmutableTable}s, to help with displaying them in + * grids. + */ +@AutoValue +public abstract class DisplayRecipe implements Comparable { + public static DisplayRecipe create(Recipe recipe, ItemRepository itemRepository) { + DisplayRecipeType displayRecipeType = DisplayRecipeType.create(recipe.getRecipeType()); + + ImmutableList displayItemInputs = + recipe.getItemInputs().stream() + .map(itemGroup -> DisplayItemGroup.buildIcon(itemGroup, itemRepository)) + .collect(ImmutableList.toImmutableList()); + ImmutableTable itemInputs = + buildIngredientsGrid( + displayRecipeType.getItemInputGrid(), displayItemInputs, recipe); + + ImmutableList displayFluidInputs = + recipe.getFluidInputs().stream() + .map(DisplayFluidGroup::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableTable fluidInputs = + buildIngredientsGrid( + displayRecipeType.getFluidInputGrid(), displayFluidInputs, recipe); + + ImmutableList displayItemOutputs = + recipe.getItemOutputs().stream() + .map(DisplayItemStackWithProbability::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableTable itemOutputs = + buildIngredientsGrid( + displayRecipeType.getItemOutputGrid(), displayItemOutputs, recipe); + + ImmutableList displayFluidOutputs = + recipe.getFluidOutputs().stream() + .map(DisplayFluidStackWithProbability::buildIcon) + .collect(ImmutableList.toImmutableList()); + ImmutableTable fluidOutputs = + buildIngredientsGrid( + displayRecipeType.getFluidOutputGrid(), displayFluidOutputs, recipe); + + return new AutoValue_DisplayRecipe( + recipe, displayRecipeType, buildIcon(recipe), + itemInputs, fluidInputs, itemOutputs, fluidOutputs); + } + + public static Icon buildIcon(Recipe recipe) { + DisplayRecipeType displayRecipeType = DisplayRecipeType.create(recipe.getRecipeType()); + + int numItemOutputs = recipe.getItemOutputs().size(); + int numFluidOutputs = recipe.getFluidOutputs().size(); + + String description = displayRecipeType.getDescription(); + String url = UrlBuilder.buildRecipeUrl(recipe); + Icon icon; + if (numItemOutputs > 0 && numFluidOutputs > 0) { + description += String.format(" (%d items, %d fluids)", numItemOutputs, numFluidOutputs); + + icon = DisplayItemStackWithProbability.buildIcon(recipe.getItemOutputs().get(0)) + .toBuilder() + .setDescription(description) + .setUrl(url) + .build(); + } else if (numItemOutputs > 0) { + description += String.format(" (%d items)", numItemOutputs); + + Icon itemIcon = + DisplayItemStackWithProbability.buildIcon(recipe.getItemOutputs().get(0)); + icon = itemIcon.toBuilder().setDescription(description).setUrl(url).build(); + } else if (numFluidOutputs > 0) { + description += String.format(" (%d fluids)", numFluidOutputs); + + Icon fluidIcon = + DisplayFluidStackWithProbability.buildIcon(recipe.getFluidOutputs().get(0)); + icon = fluidIcon.toBuilder().setDescription(description).setUrl(url).build(); + } else { + icon = Icon.builder() + .setDescription(description + " (empty)") + .setUrl(url) + .setImageFilePath(Constants.MISSING_IMAGE) + .build(); + } + return icon; + } + + public abstract Recipe getRecipe(); + public abstract DisplayRecipeType getDisplayRecipeType(); + public abstract Icon getIcon(); + public abstract ImmutableTable getItemInputs(); + public abstract ImmutableTable getFluidInputs(); + public abstract ImmutableTable getItemOutputs(); + public abstract ImmutableTable getFluidOutputs(); + + private static ImmutableTable buildIngredientsGrid( + Dimension gridDimension, Iterable ingredients, Recipe recipe) { + ImmutableTable.Builder builder = ImmutableTable.builder(); + Iterator iterator = ingredients.iterator(); + for (int row = 0; row < gridDimension.getHeight(); row++) { + for (int col = 0; col < gridDimension.getHeight(); col++) { + if (!iterator.hasNext()) { + break; + } + + Icon icon = iterator.next(); + if (icon != null) { + builder.put(row, col, icon); + } + } + } + + if (iterator.hasNext()) { + int excess = 0; + while (iterator.hasNext()) { + excess++; + iterator.next(); + } + + Main.Logger.error( + "Tried to build recipe ingredients grid with too many ingredients!\n" + + "Expected {}x{}; got {} excess for recipe {}", + gridDimension.getWidth(), gridDimension.getHeight(), excess, recipe.getId()); + } + + return builder.build(); + } + + @Override + public int compareTo(DisplayRecipe other) { + return getRecipe().compareTo(other.getRecipe()); + } +} \ No newline at end of file diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipeType.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipeType.java new file mode 100644 index 0000000..ead5ea7 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/display/recipe/DisplayRecipeType.java @@ -0,0 +1,76 @@ +package com.github.dcysteine.nesql.server.plugin.base.display.recipe; + +import com.github.dcysteine.nesql.server.display.Dimension; +import com.github.dcysteine.nesql.sql.base.recipe.RecipeType; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class DisplayRecipeType implements Comparable { + public static DisplayRecipeType create(RecipeType recipeType) { + String description; + Dimension itemInputGrid; + Dimension fluidInputGrid; + Dimension itemOutputGrid; + Dimension fluidOutputGrid; + switch (recipeType) { + case MINECRAFT_SHAPED_CRAFTING -> { + description = "Shaped Crafting Recipe"; + itemInputGrid = Dimension.create(3); + fluidInputGrid = Dimension.create(0); + itemOutputGrid = Dimension.create(1); + fluidOutputGrid = Dimension.create(0); + } + + case MINECRAFT_SHAPED_CRAFTING_OREDICT -> { + description = "Shaped Crafting Recipe (Oredict)"; + itemInputGrid = Dimension.create(3); + fluidInputGrid = Dimension.create(0); + itemOutputGrid = Dimension.create(1); + fluidOutputGrid = Dimension.create(0); + } + + case MINECRAFT_SHAPELESS_CRAFTING -> { + description = "Shapeless Crafting Recipe"; + itemInputGrid = Dimension.create(3); + fluidInputGrid = Dimension.create(0); + itemOutputGrid = Dimension.create(1); + fluidOutputGrid = Dimension.create(0); + } + + case MINECRAFT_SHAPELESS_CRAFTING_OREDICT -> { + description = "Shapeless Crafting Recipe (Oredict)"; + itemInputGrid = Dimension.create(3); + fluidInputGrid = Dimension.create(0); + itemOutputGrid = Dimension.create(1); + fluidOutputGrid = Dimension.create(0); + } + + case MINECRAFT_FURNACE -> { + description = "Furnace"; + itemInputGrid = Dimension.create(1); + fluidInputGrid = Dimension.create(0); + itemOutputGrid = Dimension.create(1); + fluidOutputGrid = Dimension.create(0); + } + + default -> + throw new IllegalArgumentException("Unrecognized recipe type: " + recipeType); + } + + return new AutoValue_DisplayRecipeType( + recipeType, description, + itemInputGrid, fluidInputGrid, itemOutputGrid, fluidOutputGrid); + } + + public abstract RecipeType getRecipeType(); + public abstract String getDescription(); + public abstract Dimension getItemInputGrid(); + public abstract Dimension getFluidInputGrid(); + public abstract Dimension getItemOutputGrid(); + public abstract Dimension getFluidOutputGrid(); + + @Override + public int compareTo(DisplayRecipeType other) { + return getRecipeType().compareTo(other.getRecipeType()); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/FluidSpecs.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/FluidSpecs.java new file mode 100644 index 0000000..00a4b4c --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/FluidSpecs.java @@ -0,0 +1,31 @@ +package com.github.dcysteine.nesql.server.plugin.base.specs; + +import com.github.dcysteine.nesql.sql.base.fluid.Fluid; +import com.github.dcysteine.nesql.sql.base.fluid.Fluid_; +import org.springframework.data.jpa.domain.Specification; + +public class FluidSpecs { + // Static class. + private FluidSpecs() {} + + /** Matches by regex. */ + public static Specification buildLocalizedNameSpec(String localizedName) { + return (root, query, builder) -> { + return builder.like(root.get(Fluid_.LOCALIZED_NAME), localizedName); + }; + } + + /** Matches by regex. */ + public static Specification buildInternalNameSpec(String internalName) { + return (root, query, builder) -> { + return builder.like(root.get(Fluid_.INTERNAL_NAME), internalName); + }; + } + + /** Matches by Minecraft item ID. */ + public static Specification buildFluidIdSpec(int fluidId) { + return (root, query, builder) -> { + return builder.equal(root.get(Fluid_.FLUID_ID), fluidId); + }; + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/ItemSpecs.java b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/ItemSpecs.java new file mode 100644 index 0000000..24732ec --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/plugin/base/specs/ItemSpecs.java @@ -0,0 +1,37 @@ +package com.github.dcysteine.nesql.server.plugin.base.specs; + +import com.github.dcysteine.nesql.sql.base.item.Item; +import com.github.dcysteine.nesql.sql.base.item.Item_; +import org.springframework.data.jpa.domain.Specification; + +public class ItemSpecs { + // Static class. + private ItemSpecs() {} + + /** Matches by regex. */ + public static Specification buildLocalizedNameSpec(String localizedName) { + return (root, query, builder) -> { + return builder.like(root.get(Item_.LOCALIZED_NAME), localizedName); + }; + } + + /** Matches by regex. */ + public static Specification buildInternalNameSpec(String internalName) { + return (root, query, builder) -> { + return builder.like(root.get(Item_.INTERNAL_NAME), internalName); + }; + } + + /** Matches by Minecraft item ID. */ + public static Specification buildItemIdSpec(int itemId) { + return (root, query, builder) -> { + return builder.equal(root.get(Item_.ITEM_ID), itemId); + }; + } + + public static Specification buildItemDamageSpec(int itemDamage) { + return (root, query, builder) -> { + return builder.equal(root.get(Item_.ITEM_DAMAGE), itemDamage); + }; + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/service/SearchService.java b/src/main/java/com/github/dcysteine/nesql/server/service/SearchService.java new file mode 100644 index 0000000..3e27e56 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/service/SearchService.java @@ -0,0 +1,43 @@ +package com.github.dcysteine.nesql.server.service; + +import com.github.dcysteine.nesql.server.config.ExternalConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Service; +import org.springframework.ui.Model; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.util.function.Function; + +/** Contains helper logic for handling searching and pagination. */ +@Service +public class SearchService { + @Autowired + private ExternalConfig externalConfig; + + public , R, D> String handleGetAll( + int page, Model model, T repository, Function buildDisplay) { + return handleSearch(page, model, repository, Specification.allOf(), buildDisplay); + } + + public , R, D> String handleSearch( + int page, Model model, + T repository, Specification spec, Function buildDisplay) { + // Pageable uses 0-index page, but we want 1-indexed. + Pageable pageable = Pageable.ofSize(externalConfig.getPageSize()).withPage(page - 1); + Page results = repository.findAll(spec, pageable).map(buildDisplay); + + String baseUri = + ServletUriComponentsBuilder.fromCurrentRequest() + .replaceQueryParam("page") + .toUriString(); + + model.addAttribute("page", results); + model.addAttribute("baseUri", baseUri); + return "search_results"; + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/util/Constants.java b/src/main/java/com/github/dcysteine/nesql/server/util/Constants.java new file mode 100644 index 0000000..a225d45 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/util/Constants.java @@ -0,0 +1,9 @@ +package com.github.dcysteine.nesql.server.util; + +public class Constants { + // Static class. + private Constants() {} + + /** Return this for cases where we lack an image to show (e.g., empty item groups). */ + public static final String MISSING_IMAGE = "missing.png"; +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/util/NbtUtil.java b/src/main/java/com/github/dcysteine/nesql/server/util/NbtUtil.java deleted file mode 100644 index 3765e1e..0000000 --- a/src/main/java/com/github/dcysteine/nesql/server/util/NbtUtil.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.dcysteine.nesql.server.util; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -/** Utility class containing methods for handling NBTs in string form. */ -public final class NbtUtil { - /** Static class. */ - private NbtUtil() {} - - /** - * The counterpart to this method is - * {@code com.github.dcysteine.nesql.exporter.util.IdUtil.encodeNbt()}. - */ - public static String decodeNbt(String encodedNbt) { - return new String(Base64.getUrlDecoder().decode(encodedNbt), StandardCharsets.UTF_8); - } -} diff --git a/src/main/java/com/github/dcysteine/nesql/server/util/NumberUtil.java b/src/main/java/com/github/dcysteine/nesql/server/util/NumberUtil.java new file mode 100644 index 0000000..9f62292 --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/util/NumberUtil.java @@ -0,0 +1,30 @@ +package com.github.dcysteine.nesql.server.util; + +import com.google.common.math.DoubleMath; + +import java.text.NumberFormat; + +public class NumberUtil { + // Static class. + private NumberUtil() {} + + public static boolean fuzzyEquals(double a, double b) { + return DoubleMath.fuzzyEquals(a, b, 0.000001d); + } + + public static String formatInteger(long integer) { + NumberFormat numberFormat = NumberFormat.getIntegerInstance(); + return numberFormat.format(integer); + } + + public static String formatCompact(long integer) { + NumberFormat numberFormat = NumberFormat.getCompactNumberInstance(); + return numberFormat.format(integer); + } + + public static String formatPercentage(double percentage) { + NumberFormat numberFormat = NumberFormat.getPercentInstance(); + numberFormat.setMaximumFractionDigits(2); + return numberFormat.format(percentage); + } +} diff --git a/src/main/java/com/github/dcysteine/nesql/server/util/UrlBuilder.java b/src/main/java/com/github/dcysteine/nesql/server/util/UrlBuilder.java new file mode 100644 index 0000000..fbf8f8e --- /dev/null +++ b/src/main/java/com/github/dcysteine/nesql/server/util/UrlBuilder.java @@ -0,0 +1,38 @@ +package com.github.dcysteine.nesql.server.util; + +import com.github.dcysteine.nesql.sql.base.fluid.Fluid; +import com.github.dcysteine.nesql.sql.base.fluid.FluidGroup; +import com.github.dcysteine.nesql.sql.base.item.Item; +import com.github.dcysteine.nesql.sql.base.item.ItemGroup; +import com.github.dcysteine.nesql.sql.base.recipe.Recipe; + +/** Helper class containing methods for building URLs. */ +public class UrlBuilder { + // Static class. + private UrlBuilder() {} + + /** Use this for cases where we lack a valid URL. */ + public static String getMissingUrl() { + return "~/notfound"; + } + + public static String buildItemUrl(Item item) { + return "~/item/" + item.getId(); + } + + public static String buildItemGroupUrl(ItemGroup itemGroup) { + return "~/itemgroup/" + itemGroup.getId(); + } + + public static String buildFluidUrl(Fluid fluid) { + return "~/fluid/" + fluid.getId(); + } + + public static String buildFluidGroupUrl(FluidGroup fluidGroup) { + return "~/fluidgroup/" + fluidGroup.getId(); + } + + public static String buildRecipeUrl(Recipe recipe) { + return "~/recipe/" + recipe.getId(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 058fe4c..d228971 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,30 @@ -spring.web.resources.static-locations=file:nesql-repository/,classpath:/static/,classpath:/static/bootstrap/ +# ExternalConfig values +nesql.server.repository-name=nesql-repository +nesql.server.page-size=25 +# Change to false to disable the /shutdown endpoint. +# It is recommended to turn this off if you allow external users. +nesql.server.enable-shutdown=true +# Change to true to allow IPs other than localhost to connect. Do this at your own risk! +nesql.server.allow-external-users=false + +# Debug options +#debug=true +#spring.jpa.show-sql=true + +# TODO set up custom ResourceResolver to make this configured by repository-name option +#spring.web.resources.static-locations=file:nesql-repository/,classpath:/static/,classpath:/static/bootstrap/ +spring.web.resources.static-locations=file:nesql-repository/,classpath:/static/ spring.jpa.hibernate.ddl-auto=none +# TODO set up something to make this configured by repository-name option spring.datasource.url=jdbc:h2:./nesql-repository/nesql-db spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -# NESQL Exporter uses Hibernate, which doesn't include underscores in column names. +# SQL naming must be consistent with what NESQL Exporter uses, which are the Hibernate defaults. spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl -spring.jpa.show-sql: true +spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl # Graceful shutdown allows us to serve the shutdown page before we actually shut down. server.shutdown=graceful diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css new file mode 100644 index 0000000..dbc6d67 --- /dev/null +++ b/src/main/resources/static/css/main.css @@ -0,0 +1,43 @@ +/* An icon is a rendered item or fluid. */ +img.icon { + width: 64px; + height: 64px; +} + +div.icon-container { + display: inline-block; + position: relative; +} + +span.icon-overlay { + position: absolute; + padding: 2px; + border-radius: 0.375rem; + color: white; + background-color: rgba(0, 0, 0, 0.5); + font-size: 16px; + text-shadow: 1px 1px 2px black; +} + +span.icon-overlay.top-left { + top: 0; + left: 0; +} + +span.icon-overlay.bottom-right { + bottom: 0; + right: 0; +} + +table.recipe-grid-table tr { + height: 70px; +} +table.recipe-grid-table td { + width: 70px; + text-align: center; + vertical-align: center; + border: 1px solid black; +} +table.recipe-grid-table td.recipe-grid-shaded { + background-color: rgba(0, 0, 0, 0.25); +} \ No newline at end of file diff --git a/src/main/resources/static/image/MissingImage.png b/src/main/resources/static/image/missing.png similarity index 100% rename from src/main/resources/static/image/MissingImage.png rename to src/main/resources/static/image/missing.png diff --git a/src/main/resources/static/js/initialize_tooltips.js b/src/main/resources/static/js/initialize_tooltips.js new file mode 100644 index 0000000..d333701 --- /dev/null +++ b/src/main/resources/static/js/initialize_tooltips.js @@ -0,0 +1,2 @@ +const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') +const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) diff --git a/src/main/resources/templates/fragment.html b/src/main/resources/templates/fragment.html new file mode 100644 index 0000000..105ead1 --- /dev/null +++ b/src/main/resources/templates/fragment.html @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + +

+ + +
+
+ +
+ + + + [[${icon.topLeft}]] + + + + [[${icon.bottomRight}]] + +
+ + +
+
+ +
+
+ +
+
+

[[${icon.description}]]

+
+
+ +
+
+ +
+
+

[[${icon.description}]]

+
+
+ + + + + + + + +
+ +
+ No rows in table +
+ +
+ + + + + +
+ + + +
+
+

+ No rows in table +

+
+ +
+
+
+ + + + + + + +
+ +
+ No rows in table +
+
+
+
+ +
+
+ Page [[${pageNumber}]] of [[${page.totalPages}]] | [[${page.totalElements}]] total results +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..88fd181 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,17 @@ + + + + +
    +
    +
    +

    + Welcome to NESQL! +

    +
    +
    + Loaded [[${itemCount}]] items, [[${fluidCount}]] fluids, [[${recipeCount}]] recipes. +
    +
    + + \ No newline at end of file diff --git a/src/main/resources/templates/not_found.html b/src/main/resources/templates/not_found.html new file mode 100644 index 0000000..2b0e531 --- /dev/null +++ b/src/main/resources/templates/not_found.html @@ -0,0 +1,10 @@ + + + + +
      + + + diff --git a/src/main/resources/templates/plugin/base/fluid/fluid.html b/src/main/resources/templates/plugin/base/fluid/fluid.html new file mode 100644 index 0000000..9a16fc8 --- /dev/null +++ b/src/main/resources/templates/plugin/base/fluid/fluid.html @@ -0,0 +1,91 @@ + + + + +
        + +
        +
        +
        +
        + +
        +
        + + + + + + + + + + + + + + + + + +
        ID[[${fluid.id}]]
        Internal Name[[${fluid.internalName}]]
        Unlocalized Name[[${fluid.unlocalizedName}]]
        Localized Name[[${fluid.localizedName}]]
        +
        +
        + + + + + + + + + +
        Fluid ID[[${fluid.fluidId}]]
        NBT[[${fluid.nbt}]]
        +
        +
        + +
        + +
        +
        +
        +
        +
        + +
        +
        +
        + +
        +
        +
        +
        +
        + + + + \ No newline at end of file diff --git a/src/main/resources/templates/plugin/base/fluid/search.html b/src/main/resources/templates/plugin/base/fluid/search.html new file mode 100644 index 0000000..01e24d9 --- /dev/null +++ b/src/main/resources/templates/plugin/base/fluid/search.html @@ -0,0 +1,44 @@ + + + + +
          + +
          +
          +

          + Search Fluids +

          +
          +
          + + Query fields will be joined with AND. Leave all fields blank to query all items. + +
          + +
          +
          +
          +
          +
          + + + + + +
          +
          + + +
          +
          +
          +
          + +
          +
          +
          +
          +
          + + diff --git a/src/main/resources/templates/plugin/base/fluidgroup/fluid_group.html b/src/main/resources/templates/plugin/base/fluidgroup/fluid_group.html new file mode 100644 index 0000000..17f6067 --- /dev/null +++ b/src/main/resources/templates/plugin/base/fluidgroup/fluid_group.html @@ -0,0 +1,47 @@ + + + + + +
            + +
            +
            +
            +
            + +
            + +
            +
            + +
            +
            +
            + +
            +
            +
            +
            +
            + + + + \ No newline at end of file diff --git a/src/main/resources/templates/plugin/base/item.html b/src/main/resources/templates/plugin/base/item.html deleted file mode 100644 index 3019c1b..0000000 --- a/src/main/resources/templates/plugin/base/item.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Item - - - -

            - -

            -

            -

            - - \ No newline at end of file diff --git a/src/main/resources/templates/plugin/base/item/item.html b/src/main/resources/templates/plugin/base/item/item.html new file mode 100644 index 0000000..657f7d0 --- /dev/null +++ b/src/main/resources/templates/plugin/base/item/item.html @@ -0,0 +1,125 @@ + + + + +

              + +
              +
              +
              +
              + +
              +
              + + + + + + + + + + + + + + + + + + + + + +
              ID[[${item.id}]]
              Mod[[${item.modId}]]
              Internal Name[[${item.internalName}]]
              Unlocalized Name[[${item.unlocalizedName}]]
              Localized Name[[${item.localizedName}]]
              +
              +
              + + + + + + + + + + + + + + + + + +
              Item ID[[${item.itemId}]]
              Item Damage[[${item.itemDamage}]]
              NBT[[${item.nbt}]]
              Tooltip[[${item.tooltip}]]
              +
              +
              + +
              + +
              +
              +
              +
              +
              + +
              +
              +
              + +
              +
              +
              + +
              +
              +
              + +
              +
              +
              +
              +
              + + + + \ No newline at end of file diff --git a/src/main/resources/templates/plugin/base/item/search.html b/src/main/resources/templates/plugin/base/item/search.html new file mode 100644 index 0000000..a035f90 --- /dev/null +++ b/src/main/resources/templates/plugin/base/item/search.html @@ -0,0 +1,47 @@ + + + + +
                + +
                +
                +

                + Search Items +

                +
                +
                + + Query fields will be joined with AND. Leave all fields blank to query all items. + +
                + +
                +
                +
                +
                +
                + + + + + +
                +
                + + + + + +
                +
                +
                +
                + +
                +
                +
                +
                +
                + + diff --git a/src/main/resources/templates/plugin/base/itemgroup/item_group.html b/src/main/resources/templates/plugin/base/itemgroup/item_group.html new file mode 100644 index 0000000..7253c23 --- /dev/null +++ b/src/main/resources/templates/plugin/base/itemgroup/item_group.html @@ -0,0 +1,68 @@ + + + + +
                  + +
                  +
                  +
                  +
                  + +
                  + +
                  +
                  + +
                  +
                  +
                  + +
                  +
                  +
                  + +
                  +
                  +
                  + +
                  +
                  +
                  +
                  +
                  + + + + \ No newline at end of file diff --git a/src/main/resources/templates/plugin/base/recipe/recipe.html b/src/main/resources/templates/plugin/base/recipe/recipe.html new file mode 100644 index 0000000..1903bf2 --- /dev/null +++ b/src/main/resources/templates/plugin/base/recipe/recipe.html @@ -0,0 +1,78 @@ + + + + +
                    + +
                    +
                    +
                    +
                    +
                    +

                    + + Shapeless + + + Shaped + +

                    +
                    + +
                    + +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    +
                    + + + + \ No newline at end of file diff --git a/src/main/resources/templates/search_results.html b/src/main/resources/templates/search_results.html new file mode 100644 index 0000000..4ccb678 --- /dev/null +++ b/src/main/resources/templates/search_results.html @@ -0,0 +1,9 @@ + + + + +
                      +
                      + + + \ No newline at end of file diff --git a/src/main/resources/templates/shutdown.html b/src/main/resources/templates/shutdown.html index d530362..a6ab0b0 100644 --- a/src/main/resources/templates/shutdown.html +++ b/src/main/resources/templates/shutdown.html @@ -1,10 +1,11 @@ - + - - Shutting Down - - + -

                      + +

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