BrowseController.java
package info.textgrid.rep.browse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.TreeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.cxf.helpers.IOUtils;
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.RequestParam;
import info.textgrid.clients.tgcrud.CrudClientException;
import info.textgrid.namespaces.middleware.tgsearch.ResultType;
import info.textgrid.namespaces.middleware.tgsearch.Revisions;
import info.textgrid.rep.i18n.I18N;
import info.textgrid.rep.i18n.I18NProvider;
import info.textgrid.rep.markdown.MarkdownRenderService;
import info.textgrid.rep.service.AggregatorClientService;
import info.textgrid.rep.service.IIIFClientService;
import info.textgrid.rep.service.ServiceConnectionException;
import info.textgrid.rep.service.TgcrudClientService;
import info.textgrid.rep.service.TgrepConfigurationService;
import info.textgrid.rep.service.TgsearchClientService;
import info.textgrid.rep.shared.Utils;
import info.textgrid.rep.shared.ToolLink;
@Controller
public class BrowseController {
private TgrepConfigurationService tgrepConfig;
private AggregatorClientService aggregatorClient;
private TgcrudClientService tgcrudClient;
private TgsearchClientService tgsearchClient;
private MarkdownRenderService markdownRenderService;
private I18NProvider i18nProvider;
private IIIFClientService iiifClientService;
private static final Log log = LogFactory.getLog(BrowseController.class);
@Autowired
public BrowseController(
TgrepConfigurationService tgrepConfig,
AggregatorClientService aggregatorClient,
TgcrudClientService tgcrudClient,
TgsearchClientService tgsearchClient,
MarkdownRenderService markdownRenderService,
IIIFClientService iiifClientService,
I18NProvider i18nProvider) {
this.tgrepConfig = tgrepConfig;
this.aggregatorClient = aggregatorClient;
this.tgcrudClient = tgcrudClient;
this.tgsearchClient = tgsearchClient;
this.iiifClientService = iiifClientService;
this.markdownRenderService = markdownRenderService;
this.i18nProvider = i18nProvider;
}
@GetMapping("/browse/{id}")
public String browse(
Locale locale,
Model model,
@PathVariable("id") String id,
@RequestParam(value = "mode", defaultValue = "list") String mode,
@RequestParam(value = "fragment", required = false) String fragment) {
boolean sandbox = this.tgrepConfig.getSandboxEnabled();
I18N i18n = i18nProvider.getI18N(locale);
// common variables for browse-root aggregations and browse single items
model.addAttribute("mode", mode);
// if no id, browse all root aggregations
if (id == null || id.equals("root")) {
List<ToolLink> viewmodes = new ArrayList<ToolLink>();
viewmodes
.add(new ToolLink(i18n.get("list"), "/browse/root?mode=list", mode.equals("list")));
viewmodes.add(new ToolLink(i18n.get("gallery"), "/browse/root?mode=gallery",
mode.equals("gallery")));
model.addAttribute("viewmodes", viewmodes);
List<ResultType> results = this.tgsearchClient.listRootCollections().getResult();
model.addAttribute("results", results);
model.addAttribute("browseRootAggregations", true);
return "browse";
}
if (!id.startsWith("textgrid:")) {
id = "textgrid:" + id;
}
id = id.replace("_", ".");
model.addAttribute("textgridUri", id);
ResultType metadata = this.tgsearchClient.getMetadata(id);
model.addAttribute("metadata", metadata);
// for path, todo: create a tagfile for path
model.addAttribute("result", metadata);
model.addAttribute("projectmap", tgsearchClient.getProjectMap(sandbox));
String format = metadata.getObject().getGeneric().getProvided().getFormat();
model.addAttribute("format", format);
/*
* Deliver different browse pages based on content type of textgridobject:
* aggregation, xml or image
*/
try {
if (format.contains("tg.aggregation") || format.contains("text/tg.work+xml")) {
handleAggregations(model, i18n, id, format, mode);
} else if (format.equals("text/xml") || format.startsWith("application/xml")) {
handleXml(model, i18n, id, metadata, fragment);
} else if (format.contains("image")) {
handleImages(model, id);
} else if (format.equals("text/markdown")) {
handleMarkdown(model, id);
} else if (format.equals("text/plain")) {
handlePlaintext(model, id);
}
} catch(ServiceConnectionException e) {
// aggregator did not respond in time
model.addAttribute("isRenderingSlow", true);
model.addAttribute("renderingMessage", e.getMessage());
model.addAttribute("renderingTimeout", tgrepConfig.getAggregatorReadTimeout());
model.addAttribute("isTEI", true);
model.addAttribute("tools", addTeiTools(id));
}
// add revisions info
listRevisions(model, id, metadata);
return "browse";
}
/**
* setup browse page to show tools metadata and the like for xml type files, which are now METS/XML or TEI
*
* @param model
* @param i18n
* @param id
* @param metadata
* @param fragment
* @throws ServiceConnectionException
*/
private void handleXml(Model model, I18N i18n, String id, ResultType metadata, String fragment) throws ServiceConnectionException {
List<ToolLink> tools = new ArrayList<ToolLink>();
// identifier@type of trier dfg-viewer mets is METSXMLID
// TODO: look into relation/tg:rootElementNamespace
if (!metadata.getObject().getGeneric().getProvided().getIdentifier().isEmpty()
&& metadata.getObject().getGeneric().getProvided().getIdentifier().get(0) != null
&& metadata.getObject().getGeneric().getProvided().getIdentifier().get(0)
.getType() != null
&& metadata.getObject().getGeneric().getProvided().getIdentifier().get(0).getType()
.equals("METSXMLID")) {
try {
String tgcrudUrl4DFGViewer = URLEncoder.encode(
tgrepConfig.getTextgridHost() + "/1.0/tgcrud-public/rest/" + id + "/data", "UTF-8");
tools.add(new ToolLink("DFG-Viewer",
"http://dfg-viewer.de/v3/?set[zoom]=min&set[mets]=" + tgcrudUrl4DFGViewer, false));
} catch (UnsupportedEncodingException e) {
log.error("error encoding url", e);
}
} else { // assume tei-xml
String teiHtml = "";
if (fragment != null) {
teiHtml = this.aggregatorClient.renderTEIFragment(id, fragment);
} else {
String xsltUri = this.tgsearchClient.getProjectXsltUri(metadata);
log.debug("xslt for project is: " + xsltUri);
model.addAttribute("projectXsltUri", xsltUri);
teiHtml = this.aggregatorClient.renderTEI(id, xsltUri);
// TODO https://gitlab.gwdg.de/dariah-de/dariah-de-common/-/issues/39 - for the meantime toc only in digibib
if(metadata.getObject().getGeneric().getGenerated().getProject().getId().equals("TGPR-372fe6dc-57f2-6cd4-01b5-2c4bbefcfd3c")){
String tocHtml = this.aggregatorClient.renderToc(id);
log.debug("toc: " + tocHtml);
model.addAttribute("tocHtml", tocHtml);
}
}
model.addAttribute("teiHtml", teiHtml);
model.addAttribute("isTEI", true);
// TEI specific tools here
tools.addAll(addTeiTools(id));
}
// Mirador link may be shown for METS/MODS or TEI files (if manifest available for this file)
if (hasIiifManifest(metadata.getObject().getGeneric().getGenerated().getProject().getId())) {
String manifestUrl = tgrepConfig.getTextgridHost() + "/1.0/iiif/manifests/" + id + "/manifest.json";
//tools.add(new ToolLink("Mirador", tgrepConfig.getToolMiradorHost() + "/?uri=" + id, false));
tools.add(new ToolLink("Mirador", "https://projectmirador.org/embed/?iiif-content=" + manifestUrl, false));
//tools.add(new ToolLink("Universal Viewer", "https://uv-v3.netlify.app/#?c=&m=&s=&cv=&manifest=" + manifestUrl, false));
tools.add(new ToolLink(
"<img style='margin-top:2px; height: 21px;' title='Drop icon on Mirador to open manifest' src='" + tgrepConfig.getTextgridHost() + "/1.0/iiif/manifests/img/iiif-logo.svg'>",
manifestUrl, false
));
}
model.addAttribute("tools", tools);
}
/**
* Some tools only useful for browsing TEI (voyant, switchboard)
* @param tools
* @param id
*/
private List<ToolLink> addTeiTools(String id) {
List<ToolLink> tools = new ArrayList<ToolLink>();
// Voyant
tools.add(
new ToolLink("Voyant", tgrepConfig.getVoyantHost() + "/?input=" + tgrepConfig.getTextgridHost() + "/1.0/tgcrud-public/rest/" + id + "/data", false)
.setHelpLink("/docs/voyant")
);
// Annotate
tools.add(
new ToolLink("Annotate", tgrepConfig.getToolAnnotateHost() + "/data.html?uri=" + id, false, "annotation-button")
.setHelpLink("/docs/annotate")
);
// CLARIN Language Resource Switchboard (LRS)
try {
//String xml4switchboard = URLEncoder.encode(tgrepConfig.getTextgridHost() + "/1.0/tgcrud-public/rest/" + id + "/data", "UTF-8");
String text4switchboard = URLEncoder.encode(tgrepConfig.getTextgridHost() + "/1.0/aggregator/text/" + id , "UTF-8");
tools.add(
new ToolLink("Switchboard", tgrepConfig.getToolSwitchboardHost() + "/" + text4switchboard + "/text%2Fplain", false)
.setHelpLink("/docs/switchboard")
);
} catch (UnsupportedEncodingException e) {
log.error("error encoding url for switchboard", e);
}
return tools;
}
/**
* setup browsing of aggregations
*
* @param model
* @param i18n
* @param id
* @param format
* @param mode
* @throws ServiceConnectionException
*/
private void handleAggregations(Model model, I18N i18n, String id, String format, String mode) throws ServiceConnectionException {
List<ToolLink> viewmodes = new ArrayList<ToolLink>();
viewmodes
.add(new ToolLink(i18n.get("list"), Utils.browseUrl(id) + "?mode=list", mode.equals("list")));
viewmodes.add(new ToolLink(i18n.get("gallery"), Utils.browseUrl(id) + "?mode=gallery",
mode.equals("gallery")));
if (format.equals("text/tg.edition+tg.aggregation+xml")) {
viewmodes.add(new ToolLink("TEI-Corpus", Utils.browseUrl(id) + "?mode=xml", mode.equals("xml")));
}
model.addAttribute("viewmodes", viewmodes);
if (mode != null && mode.equals("xml")) {
String teiHtml = this.aggregatorClient.renderTEI(id);
String tocHtml = this.aggregatorClient.renderToc(id);
model.addAttribute("teiHtml", teiHtml);
model.addAttribute("tocHtml", tocHtml);
} else {
log.debug("listing aggregation: " + id);
List<ResultType> results;
if (format.contains("text/tg.work+xml")) {
String workUri = id.contains(".") ? id : id + "." + tgsearchClient.latestRevision(id);;
results = this.tgsearchClient.getSearchClient()
.searchQuery()
.setQuery("*")
.addFilter("edition.isEditionOf:"+workUri)
.addFilter("format:text/tg.edition+tg.aggregation+xml")
.setSearchSandbox(this.tgrepConfig.getSandboxEnabled())
.execute()
.getResult();
} else {
results = this.tgsearchClient.listAggregation(id).getResult();
}
model.addAttribute("results", results);
}
}
/**
* browsing images, digilib link in tools
* @param model
* @param id
*/
private void handleImages(Model model, String id) {
model.addAttribute("image", true);
List<ToolLink> tools = new ArrayList<ToolLink>();
tools.add(new ToolLink("Digilib", tgrepConfig.getToolDigilibHost() + "/digilib.html?fn=" + id, false));
model.addAttribute("tools", tools);
}
private void handleMarkdown(Model model, String id) {
InputStream contentStream;
String content;
try {
contentStream = this.tgcrudClient.read(id);
content = markdownRenderService.renderHtml(contentStream);
} catch (CrudClientException e) {
content = "error reading data from crud";
log.error(content, e);
} catch (IOException e) {
content = "error parsing markdown";
log.error(content, e);
}
model.addAttribute("htmlContent", content);
}
private void handlePlaintext(Model model, String id) {
InputStream contentStream;
String content;
try {
contentStream = this.tgcrudClient.read(id);
content = IOUtils.toString(contentStream, StandardCharsets.UTF_8.name());
} catch (CrudClientException | IOException e) {
content = "error reading data";
log.error(content, e);
}
model.addAttribute("textContent", content);
}
private void listRevisions(Model model, String id, ResultType metadata) {
String baseUri = id.contains(".") ? id.substring(0, id.indexOf(".")) : id;
Revisions revisions = tgsearchClient.listRevisions(id);
int displayRev = metadata.getObject().getGeneric().getGenerated().getRevision();
int latestRev = Collections.max(revisions.getRevision()).intValue();
if(displayRev < latestRev) {
model.addAttribute("higherRevisionAvailable", true);
model.addAttribute("latestRevision", latestRev);
model.addAttribute("latestRevisionUri", baseUri+"."+latestRev);
}
if(revisions.getRevision().size() > 0) {
TreeMap<Integer, String> revmap = new TreeMap<Integer, String>(Comparator.reverseOrder());
for(BigInteger rev : revisions.getRevision()) {
revmap.put(rev.intValue(), baseUri + "." + rev);
}
model.addAttribute("revisions", revmap);
}
}
/**
* ask the iiif service if a project is
* configured to generate iiif manifests for
*
* moving this method to iiifClientService would bypass the spring cache
* so we keep it here (https://www.baeldung.com/spring-invoke-cacheable-other-method-same-bean)
*
* @param id the projectId
* @return true if iiif manifests are generated for this project
*/
private boolean hasIiifManifest(String id) {
if (iiifClientService.getManifests().contains(id)) {
return true;
}
return false;
}
}