Skip to content

Commit

Permalink
BXC-3284 add controller and service files (#1622)
Browse files Browse the repository at this point in the history
* BXC-3284 add controller and service files

* BXC-3284 update controller and service classes

* BXC-3284 move to web services app

* BXC-3284 update code

* BXC-3284 updating files and adding tests

* BXC-3284 fix path with URL encoding

* BXC-3284 update ID value

* BXC-3284 encode IDs for both methods

* BXC-3284 fix ID and start on tests

* Modify info.json directly as a json tree rather than going through ImageService3 class, which does not appear to have a @context attribute

* BXC-3284 fix tests

* BXC-3284 update xml and add javadocs

---------

Co-authored-by: Sharon Luong <snluong@email.lib.unc.edu>
Co-authored-by: Ben Pennell <bbpennel@email.unc.edu>
  • Loading branch information
3 people authored Nov 22, 2023
1 parent 62347ab commit 82a26f9
Show file tree
Hide file tree
Showing 6 changed files with 461 additions and 1 deletion.
2 changes: 1 addition & 1 deletion static/plugins/uv/uv_init.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
try {
let iiifurlAdaptor = new UV.IIIFURLAdaptor()
let data = iiifurlAdaptor.getInitialData({
manifest: 'jp2Proxy/' + $UV.dataset.url + '/jp2/manifest',
manifest: 'services/api/iiif/v3/' + $UV.dataset.url + '/manifest',
locales: [{ name: "en-GB" }]
});
let viewer = UV.init('jp2_viewer', data);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package edu.unc.lib.boxc.web.services.processing;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import edu.unc.lib.boxc.common.util.URIUtil;
import edu.unc.lib.boxc.web.common.exceptions.ClientAbortException;
import edu.unc.lib.boxc.web.common.utils.FileIOUtil;
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.EntityBuilder;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath;

/**
* Generates request, connects to, and streams the output from the image Server Proxy. Sets pertinent headers.
* @author bbpennel, snluong
*/
public class ImageServerProxyService {
private static final Logger LOG = LoggerFactory.getLogger(ImageServerProxyService.class);
private CloseableHttpClient httpClient;
private String imageServerProxyBasePath;
private String baseIiifv3Path;

public void setHttpClientConnectionManager(HttpClientConnectionManager manager) {

RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(2000)
.setConnectionRequestTimeout(5000)
.build();

this.httpClient = HttpClients.custom()
.setConnectionManager(manager)
.setDefaultRequestConfig(requestConfig)
.build();
}

/**
* Gets metadata from the IIIF V3 image server about the requested ID
* @param id ID of the requested object
* @param outStream out stream from the response
* @param response response object passed from the controller
* @param retryServerError the number of times to retry after failure
*/
public void getMetadata(String id, OutputStream outStream,
HttpServletResponse response, int retryServerError) {

var path = new StringBuilder(getImageServerProxyBasePath());
path.append(getImageServerEncodedId(id)).append(".jp2").append("/info.json");

int statusCode = -1;
String statusLine = null;
do {
HttpGet method = new HttpGet(path.toString());
try (CloseableHttpResponse httpResp = httpClient.execute(method)) {
statusCode = httpResp.getStatusLine().getStatusCode();
statusLine = httpResp.getStatusLine().toString();
if (statusCode == HttpStatus.SC_OK) {
if (response != null) {
response.setHeader("Content-Type", "application/json");
response.setHeader("content-disposition", "inline");

var mapper = new ObjectMapper();
var respData = mapper.readTree(httpResp.getEntity().getContent());
((ObjectNode) respData).put("id", URIUtil.join(baseIiifv3Path, id));

HttpEntity updatedRespData = EntityBuilder.create()
.setText(mapper.writeValueAsString(respData))
.setContentType(ContentType.APPLICATION_JSON).build();
httpResp.setEntity(updatedRespData);

FileIOUtil.stream(outStream, httpResp);
}
return;
}
} catch (ClientAbortException e) {
LOG.debug("User client aborted request to stream jp2 metadata for {}", id, e);
} catch (Exception e) {
LOG.error("Problem retrieving metadata for {}", path, e);
} finally {
method.releaseConnection();
}
retryServerError--;
} while (retryServerError >= 0 && (statusCode == 500 || statusCode == 404));
LOG.error("Unexpected failure while getting image server proxy path {}: {}", statusLine, path);
}

/**
* Gets the datastream from the IIIF V3 image server for the requested ID
* @param id ID of the requested object
* @param region region of the image
* @param size pixel size of the image, or max
* @param rotation degree of rotation
* @param quality quality of image
* @param format format like png or jpg
* @param outStream out stream of the response
* @param response response object passed from the controller
* @param retryServerError the number of times to retry after failure
*/
public void streamJP2(String id, String region, String size, String rotation, String quality,
String format, OutputStream outStream, HttpServletResponse response,
int retryServerError) {

StringBuilder path = new StringBuilder(getImageServerProxyBasePath());
path.append(getImageServerEncodedId(id)).append(".jp2")
.append("/" + region).append("/" + size)
.append("/" + rotation).append("/" + quality + "." + format);

HttpGet method = new HttpGet(path.toString());

try (CloseableHttpResponse httpResp = httpClient.execute(method)) {
int statusCode = httpResp.getStatusLine().getStatusCode();

if (statusCode == HttpStatus.SC_OK) {
if (response != null) {
response.setHeader("Content-Type", "image/jpeg");
response.setHeader("content-disposition", "inline");

FileIOUtil.stream(outStream, httpResp);
}
} else {
if ((statusCode == 500 || statusCode == 404) && retryServerError > 0) {
streamJP2(id, region, size, rotation, quality,
format, outStream, response, retryServerError - 1);
} else {
LOG.error("Unexpected failure: {}", httpResp.getStatusLine());
LOG.error("Path was: {}", method.getURI());
}
}
} catch (ClientAbortException e) {
LOG.debug("User client aborted request to stream jp2 for {}", id, e);
} catch (Exception e) {
LOG.error("Problem retrieving metadata for {}", path, e);
} finally {
method.releaseConnection();
}
}

/**
* Returns the image server base path with encoded IDs
* @param id
* @return
*/
public String getImageServerEncodedId(String id) {
var idPathEncoded = URLEncoder.encode(idToPath(id, 4, 2), StandardCharsets.UTF_8);
var idEncoded = URLEncoder.encode(id, StandardCharsets.UTF_8);
return idPathEncoded + idEncoded;
}

public void setImageServerProxyBasePath(String fullPath) {
this.imageServerProxyBasePath = fullPath;
}

public String getImageServerProxyBasePath() {
return imageServerProxyBasePath;
}

public void setBaseIiifv3Path(String baseIiifv3Path) {
this.baseIiifv3Path = baseIiifv3Path;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package edu.unc.lib.boxc.web.services.rest;

import edu.unc.lib.boxc.auth.api.Permission;
import edu.unc.lib.boxc.auth.api.models.AgentPrincipals;
import edu.unc.lib.boxc.auth.api.services.AccessControlService;
import edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil;
import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl;
import edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore;
import edu.unc.lib.boxc.model.api.ids.PID;
import edu.unc.lib.boxc.model.fcrepo.ids.PIDs;
import edu.unc.lib.boxc.web.common.controllers.AbstractSolrSearchController;
import edu.unc.lib.boxc.web.services.processing.ImageServerProxyService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY;

/**
* Controller that handles IIIF V3 requests to the image server
* @author snluong
*/
@Controller
public class ImageServerProxyController {
private static final Logger LOG = LoggerFactory.getLogger(ImageServerProxyController.class);

@Autowired
private ImageServerProxyService imageServerProxyService;

@Autowired
private AccessControlService accessControlService;

/**
* Determines if the user is allowed to access a JP2 datastream on the selected object.
*
* @param pid
* @return
*/
private boolean hasAccess(PID pid) {
var datastream = JP2_ACCESS_COPY.getId();

Permission permission = DatastreamPermissionUtil.getPermissionForDatastream(datastream);

AgentPrincipals agent = AgentPrincipalsImpl.createFromThread();
LOG.debug("Checking if user {} has access to {} belonging to object {}.",
agent.getUsername(), datastream, pid);
return accessControlService.hasAccess(pid, agent.getPrincipals(), permission);
}

/**
* Handles requests for individual region tiles.
* @param id
* @param region
* @param size
* @param rotation
* @param qualityFormat
* @param response
*/
@GetMapping("/iiif/v3/{id}/{region}/{size}/{rotation}/{qualityFormat:.+}")
public void getRegion(@PathVariable("id") String id,
@PathVariable("region") String region,
@PathVariable("size") String size, @PathVariable("rotation") String rotation,
@PathVariable("qualityFormat") String qualityFormat, HttpServletResponse response) {

PID pid = PIDs.get(id);
// Check if the user is allowed to view this object
if (this.hasAccess(pid)) {
try {
String[] qualityFormatArray = qualityFormat.split("\\.");
String quality = qualityFormatArray[0];
String format = qualityFormatArray[1];
response.addHeader("Access-Control-Allow-Origin", "*");
imageServerProxyService.streamJP2(
id, region, size, rotation, quality, format,
response.getOutputStream(), response, 1);
} catch (IOException e) {
LOG.error("Error retrieving streaming JP2 content for {}", id, e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
} else {
LOG.debug("Access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername());
response.setStatus(HttpStatus.FORBIDDEN.value());
}
}

/**
* Handles requests for jp2 metadata
*
* @param id
* @param response
*/
@GetMapping("/iiif/v3/{id}/info.json")
public void getMetadata(@PathVariable("id") String id, HttpServletResponse response) {
PID pid = PIDs.get(id);
// Check if the user is allowed to view this object
if (this.hasAccess(pid)) {
try {
response.addHeader("Access-Control-Allow-Origin", "*");
imageServerProxyService.getMetadata(id, response.getOutputStream(), response, 1);
} catch (IOException e) {
LOG.error("Error retrieving JP2 metadata content for {}", id, e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
} else {
LOG.debug("Image access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername());
response.setStatus(HttpStatus.FORBIDDEN.value());
}
}
}
6 changes: 6 additions & 0 deletions web-services-app/src/main/webapp/WEB-INF/service-context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@
<property name="accessControlService" ref="aclService"/>
<property name="accessCopiesService" ref="accessCopiesService"/>
</bean>

<bean id="imageServerProxyService" class="edu.unc.lib.boxc.web.services.processing.ImageServerProxyService">
<property name="imageServerProxyBasePath" value="${iiif.imageServer.base.url}"/>
<property name="baseIiifv3Path" value="${services.api.url}iiif/v3/"/>
<property name="httpClientConnectionManager" ref="httpClientConnectionManager" />
</bean>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="edu.unc.lib.boxc.web.common.utils.SerializationUtil.injectSettings"/>
Expand Down
Loading

0 comments on commit 82a26f9

Please sign in to comment.