Skip to content

Commit

Permalink
Implement advanced recipe search
Browse files Browse the repository at this point in the history
  • Loading branch information
D-Cysteine committed Feb 12, 2023
1 parent fee3917 commit a238e89
Show file tree
Hide file tree
Showing 4 changed files with 326 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/main/java/com/github/dcysteine/nesql/server/common/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;

/** Enum for organizing code relating to tables. */
Expand All @@ -16,6 +17,8 @@ public enum Table {
FLUID_GROUP(Plugin.BASE, "Fluid Group", "fluidgroup"),
RECIPE(Plugin.BASE, "Recipe", "recipe"),
RECIPE_TYPE(Plugin.BASE, "Recipe Type", "recipetype", "minRecipeCount", "1"),
/** Advanced recipe search. Does not have a {@code view} page. */
ADVANCED_RECIPE_SEARCH(Plugin.BASE, "Recipe+", "advrecipe"),

/** This table uses {@code ItemGroup}'s {@code view} page. */
ORE_DICTIONARY(Plugin.FORGE, "Ore Dictionary", "oredictionary"),
Expand Down Expand Up @@ -85,6 +88,13 @@ public String getSearchUrl() {
return getSearchUrl(defaultParams);
}

public String getSearchUrl(Map<String, String> params) {
UriComponentsBuilder builder =
UriComponentsBuilder.fromUriString(String.format("~/%s/search", getPath()));
params.forEach(builder::queryParam);
return builder.toUriString();
}

public String getSearchUrl(String... params) {
if (params.length % 2 != 0) {
throw new IllegalArgumentException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.github.dcysteine.nesql.server.plugin.base;

import com.github.dcysteine.nesql.server.common.Table;
import com.github.dcysteine.nesql.server.common.display.Icon;
import com.github.dcysteine.nesql.server.common.util.NumberUtil;
import com.github.dcysteine.nesql.server.common.util.ParamUtil;
import com.github.dcysteine.nesql.server.plugin.base.display.BaseDisplayFactory;
import com.github.dcysteine.nesql.server.plugin.base.spec.RecipeSpec;
import com.github.dcysteine.nesql.server.plugin.base.spec.RecipeTypeSpec;
import com.github.dcysteine.nesql.sql.base.recipe.Recipe;
import com.github.dcysteine.nesql.sql.base.recipe.RecipeRepository;
import com.github.dcysteine.nesql.sql.base.recipe.RecipeType;
import com.github.dcysteine.nesql.sql.base.recipe.RecipeTypeRepository;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Controller
@RequestMapping(path = "/base/advrecipe")
public class AdvancedRecipeSearchController {
@Autowired
private RecipeRepository recipeRepository;

@Autowired
private RecipeTypeRepository recipeTypeRepository;

@Autowired
private BaseDisplayFactory baseDisplayFactory;

@GetMapping(path = "/search")
public String search(
@RequestParam Map<String, String> params,
@RequestParam(required = false) Optional<String> recipeCategory,
@RequestParam(required = false) Optional<String> recipeType,
@RequestParam(required = false) Optional<String> inputItemName,
@RequestParam(required = false) Optional<String> inputItemModId,
@RequestParam(required = false) Optional<String> inputItemId,
@RequestParam(required = false) Optional<String> inputItemGroupId,
@RequestParam(required = false) Optional<String> inputFluidName,
@RequestParam(required = false) Optional<String> inputFluidModId,
@RequestParam(required = false) Optional<String> inputFluidId,
@RequestParam(required = false) Optional<String> inputFluidGroupId,
@RequestParam(required = false) Optional<String> outputItemName,
@RequestParam(required = false) Optional<String> outputItemModId,
@RequestParam(required = false) Optional<String> outputItemId,
@RequestParam(required = false) Optional<String> outputFluidName,
@RequestParam(required = false) Optional<String> outputFluidModId,
@RequestParam(required = false) Optional<String> outputFluidId,
Model model) {
List<Specification<Recipe>> specs = new ArrayList<>();
specs.add(ParamUtil.buildStringSpec(recipeCategory, RecipeSpec::buildRecipeCategorySpec));
specs.add(ParamUtil.buildStringSpec(recipeType, RecipeSpec::buildRecipeTypeSpec));
specs.add(ParamUtil.buildStringSpec(inputItemName, RecipeSpec::buildInputItemNameSpec));
specs.add(ParamUtil.buildStringSpec(inputItemModId, RecipeSpec::buildInputItemModIdSpec));
specs.add(ParamUtil.buildStringSpec(inputItemId, RecipeSpec::buildInputItemIdSpec));
specs.add(
ParamUtil.buildStringSpec(inputItemGroupId, RecipeSpec::buildInputItemGroupIdSpec));
specs.add(ParamUtil.buildStringSpec(inputFluidName, RecipeSpec::buildInputFluidNameSpec));
specs.add(ParamUtil.buildStringSpec(inputFluidModId, RecipeSpec::buildInputFluidModIdSpec));
specs.add(ParamUtil.buildStringSpec(inputFluidId, RecipeSpec::buildInputFluidIdSpec));
specs.add(
ParamUtil.buildStringSpec(
inputFluidGroupId, RecipeSpec::buildInputFluidGroupIdSpec));
specs.add(ParamUtil.buildStringSpec(outputItemName, RecipeSpec::buildOutputItemNameSpec));
specs.add(ParamUtil.buildStringSpec(outputItemModId, RecipeSpec::buildOutputItemModIdSpec));
specs.add(ParamUtil.buildStringSpec(outputItemId, RecipeSpec::buildOutputItemIdSpec));
specs.add(ParamUtil.buildStringSpec(outputFluidName, RecipeSpec::buildOutputFluidNameSpec));
specs.add(
ParamUtil.buildStringSpec(outputFluidModId, RecipeSpec::buildOutputFluidModIdSpec));
specs.add(ParamUtil.buildStringSpec(outputFluidId, RecipeSpec::buildOutputFluidIdSpec));

List<RecipeType> recipeTypes =
recipeTypeRepository.findAll(
RecipeTypeSpec.buildMinRecipeCountSpec(1), RecipeTypeSpec.DEFAULT_SORT);
ImmutableList<Icon> icons =
recipeTypes.stream()
.map(rt -> buildIcon(params, specs, rt))
.flatMap(Optional::stream)
.collect(ImmutableList.toImmutableList());
model.addAttribute("results", icons);

return "plugin/base/advrecipe/search";
}

private Optional<Icon> buildIcon(
Map<String, String> params, List<Specification<Recipe>> specs, RecipeType recipeType) {
Specification<Recipe> recipeTypeIdSpec =
RecipeSpec.buildRecipeTypeIdSpec(recipeType.getId());
Iterable<Specification<Recipe>> modifiedSpecs =
Iterables.concat(specs, Collections.singleton(recipeTypeIdSpec));
long size = recipeRepository.count(Specification.allOf(modifiedSpecs));

if (size == 0) {
return Optional.empty();
}

Map<String, String> modifiedParams = new HashMap<>(params);
modifiedParams.put("recipeTypeId", recipeType.getId());
modifiedParams.remove("recipeCategory");
modifiedParams.remove("recipeType");
String url = Table.RECIPE.getSearchUrl(modifiedParams);

Icon icon =
baseDisplayFactory.buildDisplayRecipeTypeIcon(recipeType).toBuilder()
.setUrl(url)
.setBottomRight(NumberUtil.formatInteger(size))
.build();
return Optional.of(icon);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public ImmutableList<InfoPanel> buildItemAdditionalInfo(Item item, DisplayServic
"Recipe output",
Table.RECIPE.getSearchUrl(
"outputItemId", item.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ input",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl("inputItemId", item.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ output",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl(
"outputItemId", item.getId())))
.build();

return ImmutableList.of(basePanel);
Expand All @@ -75,6 +86,18 @@ public ImmutableList<InfoPanel> buildFluidAdditionalInfo(Fluid fluid, DisplaySer
"Recipe output",
Table.RECIPE.getSearchUrl(
"outputFluidId", fluid.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ input",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl(
"inputFluidId", fluid.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ output",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl(
"outputFluidId", fluid.getId())))
.build();

return ImmutableList.of(basePanel);
Expand All @@ -97,6 +120,12 @@ public ImmutableList<InfoPanel> buildItemGroupAdditionalInfo(
"Recipe input",
Table.RECIPE.getSearchUrl(
"inputItemGroupId", itemGroup.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ input",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl(
"inputItemGroupId", itemGroup.getId())))
.build();

return ImmutableList.of(basePanel);
Expand All @@ -119,6 +148,12 @@ public ImmutableList<InfoPanel> buildFluidGroupAdditionalInfo(
"Recipe input",
Table.RECIPE.getSearchUrl(
"inputFluidGroupId", fluidGroup.getId())))
.addLink(
Link.create(
"bi-search",
"Recipe+ input",
Table.ADVANCED_RECIPE_SEARCH.getSearchUrl(
"inputFluidGroupId", fluidGroup.getId())))
.build();

return ImmutableList.of(basePanel);
Expand Down
158 changes: 158 additions & 0 deletions src/main/resources/templates/plugin/base/advrecipe/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html th:replace="~{fragment/template::template(~{::title}, ~{::content})}"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Advanced Search Recipes</title>
</head>
<body>

<th:block th:fragment="content">
<div class="container">
<div class="row">
<h1 class="display-3 text-center">
Advanced Search Recipes
</h1>
</div>
<div class="row">
<span class="text-center text-info">
Search recipes grouped by recipe type.
</span>
</div>
<div class="row">
<span class="text-center">
Query fields will be joined with AND. Leave all fields blank to retrieve everything.
</span>
</div>

<div class="row justify-content-center mt-5">
<form method="get">
<div class="container"></div>
<div class="row">
<div class="col">
<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('recipeCategory', 'Recipe category')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('recipeType', 'Recipe type')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>
</div>
<div class="col">
<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('inputItemName', 'Input item localized name')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('inputItemModId', 'Input item mod ID')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('inputItemId', 'Input item ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('inputItemGroupId', 'Input item group ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>
</div>
<div class="col">
<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('inputFluidName', 'Input fluid localized name')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('inputFluidModId', 'Input fluid mod ID')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('inputFluidId', 'Input fluid ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('inputFluidGroupId', 'Input fluid group ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>
</div>
<div class="col">
<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('outputItemName', 'Output item localized name')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('outputItemModId', 'Output item mod ID')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('outputItemId', 'Output item ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>
</div>
<div class="col">
<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('outputFluidName', 'Output fluid localized name')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchFieldRegex('outputFluidModId', 'Output fluid mod ID')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>

<div class="mt-2">
<th:block th:replace="~{fragment/search::searchField('outputFluidId', 'Output fluid ID', 'search')}">
</th:block>
<div class="form-text">&nbsp;</div>
</div>
</div>
</div>
<div class="row justify-content-center mt-3">
<div class="col-auto">
<button th:replace="~{fragment/search::searchButton}"></button>
</div>
</div>
</form>
</div>

<div class="row my-5">
<h1 class="display-3 text-center">
Search Results
</h1>
</div>
<div class="row">
<div class="card">
<div class="card-body">
<span th:replace="~{fragment/icon::iconList(${results})}">
</span>
</div>
</div>
</div>
</div>
</th:block>

</body>
</html>

0 comments on commit a238e89

Please sign in to comment.