Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BXC-3284 add controller and service files #1622

Merged
merged 13 commits into from
Nov 22, 2023
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,155 @@
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();
}

public void getMetadata(String id, OutputStream outStream,
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
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);
}

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,115 @@
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;

/**
* @author snluong
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
*/
@Controller
public class ImageServerProxyController extends AbstractSolrSearchController {
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
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
Loading