/**
* Copyright 2008 The University of North Carolina at Chapel Hill
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.unc.lib.dl.fedora;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.xml.sax.SAXException;
import edu.unc.lib.dl.acl.util.AccessGroupSet;
import edu.unc.lib.dl.acl.util.GroupsThreadStore;
import edu.unc.lib.dl.util.ContentModelHelper;
import edu.unc.lib.dl.util.TripleStoreQueryService.PathInfo;
/**
* Fedora data retrieval class used for accessing data streams and performing Mulgara queries to generate XML views of
* Fedora objects for outside usage.
*
* @author Gregory Jansen
* @author Ben Pennell
*/
public class FedoraDataService {
private static final Logger LOG = LoggerFactory.getLogger(FedoraDataService.class);
private edu.unc.lib.dl.fedora.AccessClient accessClient = null;
private edu.unc.lib.dl.fedora.ManagementClient managementClient = null;
private edu.unc.lib.dl.util.TripleStoreQueryService tripleStoreQueryService = null;
private String threadGroupPrefix = "";
private ExecutorService executor;
private int maxThreads;
private long serviceTimeout;
public FedoraDataService(){
maxThreads = 0;
serviceTimeout = 5000L;
}
public void init(){
CustomizableThreadFactory ctf = new CustomizableThreadFactory();
ctf.setThreadGroupName(threadGroupPrefix + "FDS");
ctf.setThreadNamePrefix(threadGroupPrefix + "FDSWorker-");
this.executor = Executors.newFixedThreadPool(maxThreads, ctf);
}
public void destroy(){
executor.shutdownNow();
}
public edu.unc.lib.dl.fedora.AccessClient getAccessClient() {
return accessClient;
}
public void setAccessClient(edu.unc.lib.dl.fedora.AccessClient accessClient) {
this.accessClient = accessClient;
}
/**
* Retrieves a view-inputs document containing the FOXML datastream for the object identified by simplepid
*
* @param simplepid
* @return
* @throws FedoraException
*/
public Document getFoxmlViewXML(String simplepid) throws FedoraException {
final PID pid = new PID(simplepid);
Document result = new Document();
final Element inputs = new Element("view-inputs");
result.setRootElement(inputs);
List<Callable<Content>> callables = new ArrayList<Callable<Content>>();
callables.add(new GetFoxml(pid));
this.retrieveAsynchronousResults(inputs, callables, pid, true);
return result;
}
/**
* Retrieves a view-inputs document containing the Mods datastream for the object identified by simplepid
*
* @param simplepid
* @return
* @throws FedoraException
*/
public Document getModsViewXML(String simplepid) throws FedoraException {
final PID pid = new PID(simplepid);
Document result = new Document();
final Element inputs = new Element("view-inputs");
result.setRootElement(inputs);
List<Callable<Content>> callables = new ArrayList<Callable<Content>>();
callables.add(new GetMods(pid));
this.retrieveAsynchronousResults(inputs, callables, pid, true);
return result;
}
public Document getObjectViewXML(String simplepid) throws FedoraException {
return getObjectViewXML(simplepid, false);
}
/**
* Retrieves a view-inputs document containing the FOXML, Fedora path information, parent collection pid, permissions
* and order within parent folder for the object identified by simplepid
*
* @param simplepid
* @return
* @throws FedoraException
*/
public Document getObjectViewXML(String simplepid, boolean failOnException) throws FedoraException {
final PID pid = new PID(simplepid);
Document result = new Document();
final Element inputs = new Element("view-inputs");
result.setRootElement(inputs);
List<Callable<Content>> callables = new ArrayList<Callable<Content>>();
callables.add(new GetFoxml(pid));
callables.add(new GetPathInfo(pid));
callables.add(new GetParentCollection(pid));
//callables.add(new GetPermissions(pid));
callables.add(new GetOrderWithinParent(pid));
callables.add(new GetDefaultWebObject(pid));
this.retrieveAsynchronousResults(inputs, callables, pid, failOnException);
return result;
}
private void retrieveAsynchronousResults(Element inputs, List<Callable<Content>> callables, PID pid,
boolean failOnException) throws FedoraException {
Collection<Future<Content>> futures = new ArrayList<Future<Content>>(callables.size());
if(GroupsThreadStore.getGroups() != null) {
AccessGroupSet groups = GroupsThreadStore.getGroups();
for(Callable<Content> c : callables) {
if(GroupForwardingCallable.class.isInstance(c)) {
GroupForwardingCallable rfc = (GroupForwardingCallable)c;
rfc.setGroups(groups);
}
}
}
for (Callable<Content> callable: callables){
futures.add(executor.submit(callable));
}
for (Future<Content> future : futures) {
try {
Content results = future.get(serviceTimeout, TimeUnit.MILLISECONDS);
if (results != null) {
inputs.addContent(results);
}
} catch (InterruptedException e){
LOG.warn("Attempt to get asynchronous results was interrupted for " + pid.getPid(), e);
return;
} catch (ExecutionException e) {
if (failOnException) {
if (e.getCause() instanceof FedoraException)
throw (FedoraException) e.getCause();
throw new ServiceException("Failed to get asynchronous results for " + pid.getPid(), e);
}
LOG.warn("Failed to get asynchronous results for " + pid.getPid() + ", continuing.", e);
} catch (TimeoutException e) {
if (failOnException) {
throw new ServiceException("Failed to get asynchronous results for " + pid.getPid(), e);
}
LOG.warn("Request for asynchronous results timed out for " + pid.getPid() + ", continuing.", e);
}
}
}
public edu.unc.lib.dl.fedora.ManagementClient getManagementClient() {
return managementClient;
}
public void setManagementClient(edu.unc.lib.dl.fedora.ManagementClient managementClient) {
this.managementClient = managementClient;
}
public edu.unc.lib.dl.util.TripleStoreQueryService getTripleStoreQueryService() {
return tripleStoreQueryService;
}
public void setTripleStoreQueryService(edu.unc.lib.dl.util.TripleStoreQueryService tripleStoreQueryService) {
this.tripleStoreQueryService = tripleStoreQueryService;
}
public int getMaxThreads() {
return maxThreads;
}
public void setMaxThreads(int maxThreads) {
this.maxThreads = maxThreads;
}
public long getServiceTimeout() {
return serviceTimeout;
}
public void setServiceTimeout(long serviceTimeout) {
this.serviceTimeout = serviceTimeout;
}
public String getThreadGroupPrefix() {
return threadGroupPrefix;
}
public void setThreadGroupPrefix(String threadGroupPrefix) {
this.threadGroupPrefix = threadGroupPrefix;
}
private abstract class GroupForwardingCallable implements Callable<Content> {
AccessGroupSet groups = null;
public void setGroups(AccessGroupSet groups) {
this.groups = groups;
}
protected void storeGroupsOnCurrentThread() {
LOG.debug("storing groups on thread for FedoraDataService.GroupForwardingCallable: "+groups);
GroupsThreadStore.storeGroups(groups);
}
protected void clearGroupsOnCurrentThread() {
LOG.debug("clearing groups on thread for FedoraDataService.GroupForwardingCallable");
GroupsThreadStore.clearGroups();
}
}
/**
* Retrieves FOXML and adds the results as a child of inputs.
*
*/
private class GetFoxml extends GroupForwardingCallable {
private PID pid;
public GetFoxml(PID pid) {
this.pid = pid;
}
@Override
public Content call() throws FedoraException {
try {
this.storeGroupsOnCurrentThread();
LOG.debug("HERE Get FOXML for pid " + pid.getPid());
// add foxml
Document foxml;
foxml = managementClient.getObjectXML(pid);
return foxml.getRootElement().detach();
} finally {
this.clearGroupsOnCurrentThread();
}
}
}
/**
* Retrieves object path information indicating the object hierarchy leading up to the specified object and adds the
* results as a child of inputs named "path".
*
*/
private class GetPathInfo implements Callable<Content> {
private PID pid;
public GetPathInfo(PID pid) {
this.pid = pid;
}
@Override
public Content call() {
LOG.debug("Get path info for " + pid.getPid());
// add path info
List<PathInfo> path = tripleStoreQueryService.lookupRepositoryPathInfo(pid);
if (path == null || path.size() == 0)
throw new ServiceException("No path information was returned for " + pid.getPid());
Element pathEl = new Element("path");
for (PathInfo i : path) {
Element p = new Element("object");
p.setAttribute("label", i.getLabel());
p.setAttribute("pid", i.getPid().getPid());
p.setAttribute("slug", i.getSlug());
pathEl.addContent(p);
}
return pathEl;
}
}
/**
* Retrieves Mods datastream for pid and adds the results as a child of inputs.
*
*/
private class GetMods extends GroupForwardingCallable {
private PID pid;
public GetMods(PID pid) {
this.pid = pid;
}
@Override
public Content call() throws FedoraException, ServiceException, SAXException {
// add MODS
try {
this.storeGroupsOnCurrentThread();
LOG.debug("Get mods for " + pid.getPid());
byte[] modsBytes = getAccessClient().getDatastreamDissemination(pid, "MD_DESCRIPTIVE", null).getStream();
Document mods = edu.unc.lib.dl.fedora.ClientUtils.parseXML(modsBytes);
return mods.getRootElement().detach();
} finally {
this.clearGroupsOnCurrentThread();
}
}
}
/**
* Retrieves the pid identifying the most immediate collection containing the object identified and adds the results
* as a child of inputs named "parentCollection".
*
*/
private class GetParentCollection implements Callable<Content> {
private PID pid;
public GetParentCollection(PID pid) {
this.pid = pid;
}
@Override
public Content call() {
LOG.debug("Get parent collection for " + pid.getPid());
PID parentCollection = tripleStoreQueryService.fetchParentCollection(pid);
if (parentCollection == null)
return null;
Element parentColEl = new Element("parentCollection");
parentColEl.setText(parentCollection.getPid());
return parentColEl;
}
}
/**
* Retrieves the internal sort order value for the default sort within the folder/collection containing the object
* identified by pid and stores the results as a child of inputs named "order".
*
*/
private class GetOrderWithinParent extends GroupForwardingCallable {
private PID pid;
public GetOrderWithinParent(PID pid) {
this.pid = pid;
}
@Override
public Content call() {
try {
this.storeGroupsOnCurrentThread();
LOG.debug("Get Order within Parent for " + pid.getPid());
PID container = tripleStoreQueryService.fetchContainer(pid);
byte[] structMapBytes = getAccessClient().getDatastreamDissemination(container, "MD_CONTENTS", null)
.getStream();
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
SAXParser saxParser = factory.newSAXParser();
StructMapOrderExtractor handler = new StructMapOrderExtractor(pid);
saxParser.parse(new ByteArrayInputStream(structMapBytes), handler);
if (handler.getOrder() != null) {
Element orderEl = new Element("order");
orderEl.setText(handler.getOrder());
return orderEl;
}
return null;
} catch (Exception e) {
throw new ServiceException(e);
} finally {
this.clearGroupsOnCurrentThread();
}
}
}
private class GetDefaultWebObject extends GroupForwardingCallable {
private PID pid;
public GetDefaultWebObject(PID pid) {
this.pid = pid;
}
@Override
public Content call() {
try {
this.storeGroupsOnCurrentThread();
String webObject = tripleStoreQueryService.fetchFirstBySubjectAndPredicate(pid, ContentModelHelper.CDRProperty.defaultWebObject.toString());
if (webObject != null) {
Document foxml;
foxml = managementClient.getObjectXML(new PID(webObject));
if (foxml != null){
Element webObjectElement = new Element("defaultWebObject");
webObjectElement.setAttribute("id", webObject);
webObjectElement.addContent(foxml.getRootElement().detach());
return webObjectElement;
}
}
} catch (Exception e) {
throw new ServiceException(e);
} finally {
this.clearGroupsOnCurrentThread();
}
return null;
}
}
}