SearchController.java
package info.textgrid.rep.search;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.propertyeditors.CustomCollectionEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.util.HtmlUtils;
import info.textgrid.namespaces.metadata.portalconfig._2020_06_16.Portalconfig;
import info.textgrid.namespaces.middleware.tgsearch.Response;
import info.textgrid.rep.botprotect.ProofOfWorkSession;
import info.textgrid.rep.i18n.I18N;
import info.textgrid.rep.i18n.I18NProvider;
import info.textgrid.rep.service.TgrepConfigurationService;
import info.textgrid.rep.service.TgsearchClientService;
import info.textgrid.rep.shared.Pager;
import info.textgrid.rep.shared.ToolLink;
import info.textgrid.rep.shared.Utils;
@Controller
@SessionAttributes("proofOfWorkSession")
public class SearchController {
private TgsearchClientService tgsearchClient;
private TgrepConfigurationService tgrepConfig;
private I18NProvider i18nProvider;
private static final Logger log = LoggerFactory.getLogger(SearchController.class);
private List<String> defaultFacets = Arrays.asList(new String[] {
"edition.language",
"edition.agents.author.value",
"work.genre",
"format",
"project.id"
});
@Value("${tgsearch.query.maxhits}")
private int maxhits;
@ModelAttribute("proofOfWorkSession")
public ProofOfWorkSession getProofOfWorkSession() {
return new ProofOfWorkSession();
}
@Autowired
public SearchController(
TgsearchClientService tgsearchClient,
TgrepConfigurationService tgrepConfig,
I18NProvider i18nProvider) {
this.tgsearchClient = tgsearchClient;
this.tgrepConfig = tgrepConfig;
this.i18nProvider = i18nProvider;
}
/**
* without this method the filter param passed to search could be separated at
* a comma, for example "filter:Tucholsky, Kurt" would become
* ["filter:Tucholsy", "Kurt"] but we want ["filter:Tucholsy, Kurt"]
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(List.class, new CustomCollectionEditor(List.class));
}
@GetMapping({"/search", "/search/"})
public String search(
@RequestParam(name="query", required=false, defaultValue="") String query,
@RequestParam(name="order", required=false, defaultValue="relevance") String order,
@RequestParam(name="start", required=false, defaultValue="0") int start,
@RequestParam(name="limit", required=false, defaultValue="20") int limit,
@RequestParam(name="filter", required=false) List<String> filter,
@RequestParam(value="mode", defaultValue="list") String mode,
@ModelAttribute ProofOfWorkSession proofOfWorkSession,
Locale locale,
Model model) {
model.addAttribute("query", HtmlUtils.htmlEscape(query));
model.addAttribute("order", order);
model.addAttribute("start", start);
model.addAttribute("limit", limit);
model.addAttribute("filter", filter);
// detect wildcard or fuzzy search, which is not "searchAll" wildcard (* with length > 1)
if(
(query.contains("*") && query.length() > 1)
|| (query.contains("%2A") && query.length() > 3)
|| query.contains("?")
|| query.contains("%3F")
|| query.contains("~")
){
// proofOfWorkSession.verfified will be set by AltchaController
if(!proofOfWorkSession.isVerified()) {
log.info("unverified wildcard search detected:" + query);
return "checkRobot";
} else {
log.info("wildcard search permitted: " + query);
}
}
if (! mode.equals("list") &! mode.equals("gallery")) {
mode = "list";
}
I18N i18n = i18nProvider.getI18N(locale);
boolean sandbox = this.tgrepConfig.getSandboxEnabled();
String aggregatorSandboxParam = sandbox ? "&sandbox=true" : "";
String realQueryString = query;
if(query.equals("") && filter != null) {
// the filter only request, e.g. from facet-browse
realQueryString = "*";
}
List<String> facets = null;
// is a project filter active? If yes, check for project specific facets
if(filter != null && filter.stream().anyMatch(a -> a.startsWith("project.id"))) {
String projectId = filter.stream().filter(a -> a.startsWith("project.id")).collect(Collectors.toList()).get(0).substring(11);
facets = new ArrayList<String>();
for (Entry<String, String> pfacet : getProjectFacets(projectId, locale, i18n).entrySet()) {
facets.add(pfacet.getKey());
// facet label to translation map
i18n.getTranslationMap().put(pfacet.getKey(), pfacet.getValue());
}
facets.addAll(defaultFacets);
} else {
facets = defaultFacets;
}
Response res = this.tgsearchClient.search(realQueryString, order, start, limit, facets, filter, sandbox);
if(res == null) {
return "error";
}
// TODO: set hitlimit (maxhits?) here or inside page (make it a spring bean?)
Pager pager = new Pager()
.setHits(Integer.parseInt(res.getHits()))
.setLimit(limit)
.setStart(start)
.setMaxHits(maxhits);
pager.calculatePages();
model.addAttribute("pager", pager);
List<ToolLink> viewmodes = new ArrayList<ToolLink>();
viewmodes.add(new ToolLink(i18n.get("list"), Utils.searchUrl("list", query, filter, order, start, limit), mode.equals("list")));
viewmodes.add(new ToolLink(i18n.get("gallery"), Utils.searchUrl("gallery", query, filter, order, start, limit), mode.equals("gallery")));
model.addAttribute("viewmodes", viewmodes);
model.addAttribute("mode", mode);
model.addAttribute("results", res.getResult());
model.addAttribute("facetResponse", res.getFacetResponse());
model.addAttribute("projectmap", tgsearchClient.getProjectMap(sandbox));
model.addAttribute("filterQueryString", Utils.getFilterQueryString(filter));
model.addAttribute("aggregatorSandboxParam", aggregatorSandboxParam);
model.addAttribute("realQueryString", realQueryString);
// which .jsp to render
return "search";
}
/**
* get project specific facets config from portalconfig if available for given projectId
*
* @param projectId the projectId
* @param locale the locale for facet label selection
* @param i18n translation array, for adding labels of project specific facets
* @return facets and their labels for the locale, or empty hashmap if nothing found
*/
private Map<String, String> getProjectFacets(String projectId, Locale locale, I18N i18n) {
// try to find project config in tgsearch
Portalconfig projectConfig = this.tgsearchClient.getProjectConfig(projectId, false).getPortalconfig();
// return empty hashmap if nothing found
if (projectConfig == null || projectConfig.getFacets() == null || projectConfig.getFacets().getFacet() == null) {
return new HashMap<String, String>();
}
// portalconfig file contains XML with JAXB databinding, we want to only get the facet config, and
// map the select elements to their given title in a new hashmap
Map<String, String> facetConf = projectConfig.getFacets().getFacet().stream()
.collect(
Collectors.toMap(
a -> a.getSelect(),
a -> a.getTitle().stream()
// get title for lang code (no fallback yet if no value for lang in projectconf)
.filter(b -> b.getLang().equals(locale.getLanguage()))
.map(b -> b.getValue())
.collect(Collectors.toList()).get(0)
)
);
Map<String, String> labels = getProjectFacetLabels(projectConfig, locale.getLanguage());
i18n.getTranslationMap().putAll(labels);
return facetConf;
}
/**
* get a hashmap of pdf_ (project defined facets) prefixed i18n keys and labels for chosen
* language from given projectconfig
*
* @param projectConfig the projectconfig
* @param lang language to select labels for
* @return hashmap of id's (pdf_ prefixed) and labels for i18n translationmap
*/
private Map<String, String> getProjectFacetLabels(Portalconfig projectConfig, String lang) {
Map<String, String> labels = projectConfig.getFacets().getFacet().stream()
.flatMap(a -> a.getLabel().stream().filter(b -> b.getLang().equals(lang))
.flatMap(v -> Stream.of(new SimpleEntry<String, String>("pdf_" + v.getFor(), v.getValue()))))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
return labels;
}
}