/** * 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 static edu.unc.lib.dl.util.ContentModelHelper.Administrative_PID.REPOSITORY; import static edu.unc.lib.dl.util.ContentModelHelper.Datastream.MD_EVENTS; import static edu.unc.lib.dl.util.ContentModelHelper.Datastream.RELS_EXT; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.apache.commons.io.FileUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.Namespace; import org.jdom2.input.SAXBuilder; import org.jdom2.output.XMLOutputter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.client.WebServiceFaultException; import org.springframework.ws.client.WebServiceIOException; import org.springframework.ws.client.core.WebServiceMessageCallback; import org.springframework.ws.client.core.WebServiceTemplate; import org.springframework.ws.soap.SoapMessage; import org.springframework.ws.soap.client.SoapFaultClientException; import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; import org.springframework.ws.transport.http.HttpComponentsMessageSender; 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.fedora.AuthorizationException.AuthorizationErrorType; import edu.unc.lib.dl.fedora.types.AddDatastream; import edu.unc.lib.dl.fedora.types.AddDatastreamResponse; import edu.unc.lib.dl.fedora.types.ArrayOfString; import edu.unc.lib.dl.fedora.types.Datastream; import edu.unc.lib.dl.fedora.types.Export; import edu.unc.lib.dl.fedora.types.ExportResponse; import edu.unc.lib.dl.fedora.types.GetDatastream; import edu.unc.lib.dl.fedora.types.GetDatastreamResponse; import edu.unc.lib.dl.fedora.types.GetNextPID; import edu.unc.lib.dl.fedora.types.GetNextPIDResponse; import edu.unc.lib.dl.fedora.types.GetObjectXML; import edu.unc.lib.dl.fedora.types.GetObjectXMLResponse; import edu.unc.lib.dl.fedora.types.Ingest; import edu.unc.lib.dl.fedora.types.IngestResponse; import edu.unc.lib.dl.fedora.types.MIMETypedStream; import edu.unc.lib.dl.fedora.types.ModifyDatastreamByReference; import edu.unc.lib.dl.fedora.types.ModifyDatastreamByReferenceResponse; import edu.unc.lib.dl.fedora.types.ModifyDatastreamByValue; import edu.unc.lib.dl.fedora.types.ModifyDatastreamByValueResponse; import edu.unc.lib.dl.fedora.types.ModifyObject; import edu.unc.lib.dl.fedora.types.ModifyObjectResponse; import edu.unc.lib.dl.fedora.types.ObjectProfile; import edu.unc.lib.dl.fedora.types.PurgeDatastream; import edu.unc.lib.dl.fedora.types.PurgeDatastreamResponse; import edu.unc.lib.dl.fedora.types.PurgeObject; import edu.unc.lib.dl.fedora.types.PurgeObjectResponse; import edu.unc.lib.dl.fedora.types.SetDatastreamVersionable; import edu.unc.lib.dl.fedora.types.SetDatastreamVersionableResponse; import edu.unc.lib.dl.httpclient.HttpClientUtil; import edu.unc.lib.dl.util.ContentModelHelper; import edu.unc.lib.dl.util.IllegalRepositoryStateException; import edu.unc.lib.dl.util.PremisEventLogger; import edu.unc.lib.dl.xml.JDOMNamespaceUtil; import edu.unc.lib.dl.xml.RDFXMLUtil; public class ManagementClient extends WebServiceTemplate { private AccessClient accessClient = null; private HttpClientConnectionManager httpManager; private CloseableHttpClient httpClient; private static int RELS_EXT_RETRIES = 10; private static long RELS_EXT_RETRY_DELAY = 250L; // ENUMS private enum Action { addDatastream("addDatastream"), getDatastream("getDatastream"), addRelationship("addRelationship"), export( "export"), getNextPID("getNextPID"), ingest("ingest"), modifyDatastreamByValue("modifyDatastreamByValue"), modifyDatastreamByReference( "modifyDatastreamByReference"), modifyObject("modifyObject"), purgeDatastream("purgeDatastream"), purgeObject( "purgeObject"), purgeRelationship("purgeRelationship"), getObjectXML("getObjectXML"), setDatastreamVersionable( "setDatastreamVersionable"); String uri = null; Action(String action) { uri = "http://www.fedora.info/definitions/1/0/api/#" + action; } WebServiceMessageCallback callback() { return new WebServiceMessageCallback() { @Override public void doWithMessage(WebServiceMessage message) { ((SoapMessage) message).setSoapAction(uri); } }; } } public enum ChecksumType { DEFAULT("DEFAULT"), DISABLED("DISABLED"), HAVAL("HAVAL"), MD5("MD5"), SHA_1("SHA-1"), SHA_256("SHA-256"), SHA_385( "SHA-385"), SHA_512("SHA-512"), TIGER("TIGER"), WHIRLPOOL("WHIRLPOOL"); private final String id; ChecksumType(String id) { this.id = id; } @Override public String toString() { return this.id; } } public enum Context { ARCHIVE("archive"), MIGRATE("migrate"), PUBLIC("public"); private final String id; Context(String id) { this.id = id; } @Override public String toString() { return this.id; } } public enum Format { ATOM_1_0("ATOM-1.0"), FOXML_1_0("FOXML-1.0"), FOXML_1_1("FOXML-1.1"), METS_FED_EXT_1_1("METSFedoraExt-1.1"); private final String id; Format(String id) { this.id = "info:fedora/fedora-system:" + id; } @Override public String toString() { return this.id; } } public enum State { ACTIVE("A"), INACTIVE("I"), DELETED("D"); private final String id; State(String id) { this.id = id; } @Override public String toString() { return this.id; } } private static final Logger log = LoggerFactory.getLogger(ManagementClient.class); private String fedoraContextUrl; private String password; private String username; private String fedoraHost; public String addManagedDatastream(PID pid, String dsid, boolean force, String message, List<String> altids, String label, boolean versionable, String mimetype, String locationURI) throws FedoraException { AddDatastream req = new AddDatastream(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setLogMessage(message); req.setDsState(State.ACTIVE.id); req.setControlGroup("M"); req.setDsLocation(locationURI); // req.setChecksum("none"); req.setChecksumType(ChecksumType.MD5.id); ArrayOfString alts = new ArrayOfString(); alts.getItem().addAll(altids); req.setAltIDs(alts); req.setDsLabel(label); req.setFormatURI(""); req.setMIMEType(mimetype); req.setVersionable(versionable); AddDatastreamResponse resp = (AddDatastreamResponse) this.callService(req, Action.addDatastream); String id = resp.getDatastreamID(); // String timestamp = this.modifyInlineXMLDatastream(pid, dsid, // force, message, altids, label, // xml); return id; } public String addInlineXMLDatastream(PID pid, String dsid, boolean force, String message, List<String> altids, String label, boolean versionable, Document xml) throws FedoraException, FileNotFoundException { File file = ClientUtils.writeXMLToTempFile(xml); return addInlineXMLDatastream(pid, dsid, force, message, altids, label, versionable, file); } public String addInlineXMLDatastream(PID pid, String dsid, boolean force, String message, List<String> altids, String label, boolean versionable, File contentFile) throws FedoraException, FileNotFoundException { String location = null; location = this.upload(contentFile); contentFile.delete(); AddDatastream req = new AddDatastream(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setLogMessage(message); req.setDsState(State.ACTIVE.id); req.setControlGroup(ContentModelHelper.ControlGroup.INTERNAL.getAttributeValue()); req.setDsLocation(location); req.setChecksumType(ChecksumType.MD5.id); ArrayOfString alts = new ArrayOfString(); alts.getItem().addAll(altids); req.setAltIDs(alts); req.setDsLabel(label); req.setFormatURI(""); req.setMIMEType("text/xml"); req.setVersionable(versionable); AddDatastreamResponse resp = (AddDatastreamResponse) this.callService(req, Action.addDatastream); String id = resp.getDatastreamID(); return id; } public void addLiteralStatement(PID pid, String pred, Namespace ns, String literal, String datatype) throws FedoraException { addTriple(pid, pred, ns, true, literal, datatype); } public void addTriple(PID pid, String pred, Namespace ns, boolean isLiteral, String value, String datatype) throws FedoraException { do { DatastreamDocument dsDoc = getRELSEXTWithRetries(pid); try { Document doc = dsDoc.getDocument(); RDFXMLUtil.addTriple(doc.getRootElement(), pred, ns, isLiteral, value, datatype); modifyDatastream(pid, RELS_EXT.getName(), "Setting exclusive relation", dsDoc.getLastModified(), dsDoc.getDocument()); return; } catch (OptimisticLockException e) { log.debug("Unable to update RELS-EXT for {}, retrying", pid, e); } } while (true); } /** * Selectively turn versioning on or off for selected datastream. When versioning is disabled, subsequent * modifications to the datastream replace the current datastream contents and no versioning history is preserved. To * put it another way: No new datastream versions will be made, but all the existing versions will be retained. All * changes to the datastream will be to the current version. * * @param pid * The PID of the object. * @param dsid * The datastream ID. * @param versionable * Enable versioning of the datastream. * @param message * A log message. * @return timestamp of the current version * @throws FedoraException */ public String setDatastreamVersionable(PID pid, String dsid, boolean versionable, String message) throws FedoraException { SetDatastreamVersionable req = new SetDatastreamVersionable(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setVersionable(versionable); req.setLogMessage(message); SetDatastreamVersionableResponse resp = (SetDatastreamVersionableResponse) this.callService(req, Action.setDatastreamVersionable); return resp.getModifiedDate(); } public boolean addObjectRelationship(PID pid, String predicate, Namespace ns, PID pid2) throws FedoraException { addTriple(pid, predicate, ns, false, pid2.getURI(), null); return true; } public Datastream getDatastream(PID pid, String dsID) throws FedoraException { GetDatastream req = new GetDatastream(); req.setPid(pid.getPid()); req.setDsID(dsID); req.setAsOfDateTime(""); GetDatastreamResponse resp = (GetDatastreamResponse) this.callService(req, Action.getDatastream); return resp.getDatastream(); } public Datastream getDatastream(PID pid, String dsID, String asOfDateTime) throws FedoraException { GetDatastream req = new GetDatastream(); req.setPid(pid.getPid()); req.setDsID(dsID); req.setAsOfDateTime(asOfDateTime); GetDatastreamResponse resp = (GetDatastreamResponse) this.callService(req, Action.getDatastream); return resp.getDatastream(); } public Boolean dataStreamExists(PID pid, String dsID) { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(this.getFedoraContextUrl()); builder.pathSegment("objects"); builder.pathSegment(pid.getPid()); builder.pathSegment("datastreams"); builder.pathSegment(dsID); builder.pathSegment("content"); HttpHead method = new HttpHead(builder.build().encode().toUriString()); try (CloseableHttpResponse httpResp = httpClient.execute(method)) { int statusCode = httpResp.getStatusLine().getStatusCode(); if (statusCode == 200) { return true; } return false; } catch (Exception e) { log.debug("HEAD request could not be completed", e); return false; } } private Object callService(Object request, Action action) throws FedoraException { return callService(request, action, true); } private Object callService(Object request, Action action, boolean retry) throws FedoraException { Object response = null; try { response = this.marshalSendAndReceive(request, action.callback()); } catch (WebServiceIOException e) { // Connection reset: Apache restarted during a call to ingest (at midnight) // 503: an error indicating that Apache is already restarting if (e.getMessage() != null && (e.getMessage().contains("503") || e.getMessage().contains("Connection reset"))) { throw new FedoraTimeoutException(e); } else if (java.net.SocketTimeoutException.class.isInstance(e.getCause())) { throw new FedoraTimeoutException(e); } else { throw new ServiceException(e); } } catch (SoapFaultClientException e) { log.debug("GOT SoapFaultClientException", e); try { FedoraFaultMessageResolver.resolveFault(e); } catch (AuthorizationException ae) { if (retry && AuthorizationErrorType.NOT_APPLICABLE.equals(ae.getType())) { log.warn("Authorization was not applicable, attempting to reestablish connection to Fedora."); try { this.initializeConnections(); } catch (Exception e1) { log.error("Failed to reestablish connection to Fedora", e); throw ae; } return callService(request, action, false); } throw ae; } } catch (WebServiceFaultException e) { throw new ServiceException("Failed to call service", e); } return response; } public Document export(Context context, Format format, String pid) throws FedoraException { byte[] data = this.exportRaw(context, format, pid); Document result = null; try { result = ClientUtils.parseXML(data); } catch (SAXException e) { throw new ServiceException("Could not parse reply.", e); } return result; } public byte[] exportRaw(Context context, Format format, String pid) throws FedoraException { Export o = new Export(); o.setContext(context.id); o.setFormat(format.id); o.setPid(pid); ExportResponse response = (ExportResponse) this.callService(o, Action.export); return response.getObjectXML(); } public String getFedoraContextUrl() { return fedoraContextUrl; } public List<PID> getNextPID(int number, String namespace) throws FedoraException { List<PID> result = new ArrayList<PID>(); GetNextPID req = new GetNextPID(); req.setNumPIDs(BigInteger.valueOf(number)); if (namespace != null) { req.setPidNamespace(namespace); } GetNextPIDResponse resp = (GetNextPIDResponse) this.callService(req, Action.getNextPID); List<String> pids = resp.getPid(); for (String pid : pids) { result.add(new PID(pid)); } return result; } public Document getObjectXML(PID pid) throws FedoraException { GetObjectXML req = new GetObjectXML(); req.setPid(pid.getPid()); GetObjectXMLResponse resp = (GetObjectXMLResponse) this.callService(req, Action.getObjectXML); try { return ClientUtils.parseXML(resp.getObjectXML()); } catch (SAXException e) { throw new FedoraException("Fedora Object XML could not be parsed"); } } public String getPassword() { return password; } public String getUsername() { return username; } public PID ingest(Document xml, Format format, String message) throws FedoraException { return ingestRaw(ClientUtils.serializeXML(xml), format, message); } public PID ingestRaw(byte[] xmlData, Format format, String message) throws FedoraException { Ingest ingest = new Ingest(); ingest.setFormat(format.id); ingest.setLogMessage(message); ingest.setObjectXML(xmlData); IngestResponse response = (IngestResponse) this.callService(ingest, Action.ingest); PID result = new PID(response.getObjectPID()); return result; } public void init() throws Exception { httpManager = new PoolingHttpClientConnectionManager(); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000) .build(); HttpClientBuilder builder = HttpClientUtil .getAuthenticatedClientBuilder(fedoraHost, getUsername(), getPassword()); builder.setDefaultRequestConfig(requestConfig); httpClient = builder.build(); initializeConnections(); } private void initializeConnections() throws Exception { SaajSoapMessageFactory msgFactory = new SaajSoapMessageFactory(); msgFactory.afterPropertiesSet(); this.setMessageFactory(msgFactory); Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("edu.unc.lib.dl.fedora.types"); marshaller.afterPropertiesSet(); this.setMarshaller(marshaller); this.setUnmarshaller(marshaller); HttpComponentsMessageSender sender = new HttpComponentsMessageSender(); UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(this.username, this.password); sender.setCredentials(credentials); sender.setReadTimeout(300 * 1000); sender.afterPropertiesSet(); this.setMessageSender(sender); // this.setFaultMessageResolver(new FedoraFaultMessageResolver()); this.setDefaultUri(this.getFedoraContextUrl() + "/services/management"); this.afterPropertiesSet(); } public void destroy() { if (this.httpManager != null) this.httpManager.shutdown(); } // DEPENDENCY SETTERS AND GETTERS public String modifyDatastreamByValue(PID pid, String dsid, boolean force, String message, List<String> altids, String label, String mimetype, String checksum, ChecksumType checksumType, File contentFile) throws FedoraException { byte[] contentBytes; try { contentBytes = FileUtils.readFileToByteArray(contentFile); return modifyDatastreamByValue(pid, dsid, force, message, altids, label, mimetype, checksum, checksumType, contentBytes); } catch (IOException e) { log.error("Could not read the new content file", e); } return null; } public String modifyDatastreamByValue(PID pid, String dsid, boolean force, String message, List<String> altids, String label, String mimetype, String checksum, ChecksumType checksumType, byte[] content) throws FedoraException { // TODO: add checksum calculation ModifyDatastreamByValue req = new ModifyDatastreamByValue(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setForce(force); req.setLogMessage(message); ArrayOfString alts = new ArrayOfString(); if (altids != null) { alts.getItem().addAll(altids); } req.setAltIDs(alts); if (label != null) { req.setDsLabel(label); } req.setFormatURI(""); if (mimetype != null) { req.setMIMEType(mimetype); } if (checksum != null) { req.setChecksum(checksum); } // else { // req.setChecksum("none"); // } if (checksumType != null) { req.setChecksumType(checksumType.id); } else { req.setChecksumType(ChecksumType.MD5.id); } req.setDsContent(content); ModifyDatastreamByValueResponse resp = (ModifyDatastreamByValueResponse) this.callService(req, Action.modifyDatastreamByValue); return resp.getModifiedDate(); } public String modifyDatastreamByReference(PID pid, String dsid, boolean force, String message, List<String> altids, String label, String mimetype, String checksum, ChecksumType checksumType, String dsLocation) throws FedoraException { // TODO: add checksum calculation ModifyDatastreamByReference req = new ModifyDatastreamByReference(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setForce(force); req.setLogMessage(message); ArrayOfString alts = new ArrayOfString(); if (altids != null) { alts.getItem().addAll(altids); } req.setAltIDs(alts); if (label != null) { req.setDsLabel(label); } req.setFormatURI(""); if (mimetype != null) { req.setMIMEType(mimetype); } if (checksum != null) { req.setChecksum(checksum); } // } else { // req.setChecksum("none"); // } if (checksumType != null) { req.setChecksumType(checksumType.id); } else { req.setChecksumType(ChecksumType.MD5.id); } req.setDsLocation(dsLocation); ModifyDatastreamByReferenceResponse resp = (ModifyDatastreamByReferenceResponse) this.callService(req, Action.modifyDatastreamByReference); return resp.getModifiedDate(); } public String modifyInlineXMLDatastream(PID pid, String dsid, boolean force, String message, List<String> altids, String label, Document content) throws FedoraException { byte[] data = ClientUtils.serializeXML(content); // String checksum = new Checksum().getChecksum(data); String timestamp = this.modifyDatastreamByValue(pid, dsid, force, message, altids, label, "text/xml", null, ChecksumType.MD5, data); return timestamp; } public void modifyDatastream(PID pid, String dsid, String message, String lastModifiedDate, Document content) throws FedoraException { byte[] dsBytes = ClientUtils.serializeXML(content); modifyDatastream(pid, dsid, message, lastModifiedDate, dsBytes); } public void modifyDatastream(PID pid, String dsid, String message, String lastModifiedDate, byte[] content) throws FedoraException { // PutMethod ignores query parameters, so put them in the URI: UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(this.getFedoraContextUrl()); builder.pathSegment("objects"); builder.pathSegment(pid.getPid()); builder.pathSegment("datastreams"); builder.pathSegment(dsid); if (message != null) { builder.queryParam("logMessage", message); } if (lastModifiedDate != null) { builder.queryParam("lastModifiedDate", lastModifiedDate); } HttpPut method = new HttpPut(builder.build().encode().toUriString()); method.setEntity(new ByteArrayEntity(content)); if (GroupsThreadStore.getGroups() != null) { method.addHeader(HttpClientUtil.FORWARDED_GROUPS_HEADER, GroupsThreadStore.getGroups().joinAccessGroups(";", null, false)); } try (CloseableHttpResponse httpResp = httpClient.execute(method)) { int statusCode = httpResp.getStatusLine().getStatusCode(); if (statusCode == 403) { throw new AuthorizationException("Failed to update datastream " + dsid + " on object " + pid + " due to insufficient permissions"); } if (statusCode == 409) { throw new OptimisticLockException("Datastream " + dsid + " on object " + pid + " has been modified more recently than the specified last modified date"); } } catch (IOException e) { throw new ServiceException("Failed to modify datastream " + dsid + " on object " + pid, e); } } public String modifyObject(PID pid, String label, String ownerid, State state, String message) throws FedoraException { ModifyObject req = new ModifyObject(); req.setLabel(label); req.setLogMessage(message); req.setOwnerId(ownerid); req.setPid(pid.getPid()); req.setState(state == null ? null : state.id); ModifyObjectResponse resp = (ModifyObjectResponse) this.callService(req, Action.modifyObject); return resp.getModifiedDate(); } public List<String> purgeDatastream(PID pid, String dsid, String message, boolean force, String start, String end) throws FedoraException { PurgeDatastream req = new PurgeDatastream(); req.setPid(pid.getPid()); req.setDsID(dsid); req.setForce(force); req.setLogMessage(message); req.setStartDT(start); req.setEndDT(end); PurgeDatastreamResponse resp = (PurgeDatastreamResponse) this.callService(req, Action.purgeDatastream); return resp.getPurgedVersionDate(); } public boolean purgeLiteralStatement(PID pid, String predicate, Namespace ns, String literal, String datatype) throws FedoraException { return purgeTriple(pid, predicate, ns, true, literal, datatype); } public String purgeObject(PID pid, String message, boolean force) throws FedoraException { PurgeObject req = new PurgeObject(); req.setPid(pid.getPid()); req.setLogMessage(message); req.setForce(force); PurgeObjectResponse resp = (PurgeObjectResponse) this.callService(req, Action.purgeObject); return resp.getPurgedDate(); } public boolean purgeObjectRelationship(PID pid, String predicate, Namespace ns, PID pid2) throws FedoraException { return purgeTriple(pid, predicate, ns, false, pid2.getURI(), null); } public boolean purgeTriple(PID pid, String predicate, Namespace ns, boolean isLiteral, String value, String datatype) throws FedoraException { do { DatastreamDocument dsDoc = getRELSEXTWithRetries(pid); try { Document doc = dsDoc.getDocument(); boolean removed = RDFXMLUtil.removeTriple(doc.getRootElement(), predicate, ns, isLiteral, value, datatype); if (removed) { modifyDatastream(pid, RELS_EXT.getName(), "Setting exclusive relation", dsDoc.getLastModified(), dsDoc.getDocument()); } return removed; } catch (OptimisticLockException e) { log.debug("Unable to update RELS-EXT for {}, retrying", pid, e); } } while (true); } public void setFedoraContextUrl(String fedoraContextUrl) { this.fedoraContextUrl = fedoraContextUrl; } public void setPassword(String password) { this.password = password; } public void setUsername(String username) { this.username = username; } public String upload(File file) throws FileNotFoundException { return upload(new FileInputStream(file), true); } public String upload(String content) { return upload(new ByteArrayInputStream(content.getBytes()), true); } public String upload(Document xml) { // write the document to a byte array XMLOutputter out = new XMLOutputter(); try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) { out.output(xml, baos); return upload(new ByteArrayInputStream(baos.toByteArray()), true); } catch (IOException e) { throw new ServiceException("Unexpected error writing to byte array output stream", e); } } public String upload(byte[] bytes) { return upload(new ByteArrayInputStream(bytes), true); } public String upload(InputStream content, boolean retry) { String result = null; String uploadURL = this.getFedoraContextUrl() + "/upload"; RequestConfig conf = RequestConfig.custom() .setExpectContinueEnabled(false) .build(); HttpPost post = new HttpPost(uploadURL); post.setConfig(conf); log.debug("Uploading file with forwarded groups: {}", GroupsThreadStore.getGroupString()); post.addHeader(HttpClientUtil.FORWARDED_GROUPS_HEADER, GroupsThreadStore.getGroupString()); log.debug("Uploading to {}", uploadURL); // Add the file to the request. It must be labeled 'file' for fedora to find it HttpEntity fileEntity = MultipartEntityBuilder.create() .addBinaryBody("file", content) .build(); post.setEntity(fileEntity); try (CloseableHttpResponse httpResp = httpClient.execute(post)) { int statusCode = httpResp.getStatusLine().getStatusCode(); String responseString = EntityUtils.toString(httpResp.getEntity(), "UTF-8"); switch (statusCode) { case HttpStatus.SC_OK: case HttpStatus.SC_CREATED: case HttpStatus.SC_ACCEPTED: result = responseString.trim(); log.info("Upload complete, response=" + result); break; case HttpStatus.SC_FORBIDDEN: log.warn("Authorization to Fedora failed, attempting to reestablish connection."); try { this.initializeConnections(); return upload(content, false); } catch (Exception e) { log.error("Failed to reestablish connection to Fedora", e); } break; case HttpStatus.SC_SERVICE_UNAVAILABLE: throw new FedoraTimeoutException("Fedora service unavailable, upload failed"); default: log.warn("Upload failed, response=" + statusCode); log.debug(responseString.toString().trim()); break; } } catch (ServiceException ex) { throw ex; } catch (Exception ex) { throw new ServiceException(ex); } return result; } public String getIrodsPath(String dsFedoraLocationToken) { log.debug("getting iRODS path for {}", dsFedoraLocationToken); String result = null; List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("pid", dsFedoraLocationToken)); StringBuilder url = new StringBuilder(); url.append(getFedoraContextUrl()).append("/storagelocation?") .append(URLEncodedUtils.format(params, "UTF-8")); HttpGet get = new HttpGet(url.toString()); try (CloseableHttpResponse httpResp = httpClient.execute(get)) { int statusCode = httpResp.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { throw new RuntimeException("CDR storage location GET method failed: " + httpResp.getStatusLine()); } else { log.debug("CDR storage location GET method: " + httpResp.getStatusLine()); result = EntityUtils.toString(httpResp.getEntity(), "UTF-8").trim(); } } catch (Exception e) { throw new ServiceException("Error while contacting iRODS location service with datastream location " + dsFedoraLocationToken, e); } return result; } /** * Poll Fedora until the PID is found or timeout. This method will blocking until at most the specified timeout plus * the timeout of the underlying HTTP connection. * * @param pid * the PID to look for * @param delay * the delay between Fedora requests in seconds * @param timeout * the total polling time in seconds * @return true when the object is found within timeout, false on timeout */ public boolean pollForObject(PID pid, int delay, int timeout) throws InterruptedException { long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime < timeout * 1000) { if (Thread.interrupted()) throw new InterruptedException(); try { ObjectProfile doc = this.getAccessClient().getObjectProfile(pid, null); if (doc != null) return true; } catch (ServiceException e) { if (log.isDebugEnabled()) log.debug("Expected service exception while polling fedora", e); } catch (FedoraException e) { // fedora responded, but object not found log.debug("got exception from fedora", e); } if (Thread.interrupted()) throw new InterruptedException(); log.info(pid + " not found, waiting " + delay + " seconds.."); Thread.sleep(delay * 1000); } return false; } /** * Returns true if the repository is available for connections * * @return */ public boolean isRepositoryAvailable() { try { ObjectProfile doc = this.getAccessClient().getObjectProfile(REPOSITORY.getPID(), null); return doc != null; } catch (Exception e) { // If an exception occurs, then the repository is not reachable } return false; } /** * Blocks until the repository is accessible or the process is interrupted */ public void waitForRepositoryAvailable() { while (!isRepositoryAvailable()) { try { log.info("Waiting for the repository to become available"); Thread.sleep(10000L); } catch (InterruptedException e) { return; } } } public String writePremisEventsToFedoraObject(PremisEventLogger eventLogger, PID pid) throws FedoraException { Document dom = null; boolean newDatastream = false; try { MIMETypedStream mts = this.getAccessClient().getDatastreamDissemination(pid, "MD_EVENTS", null); ByteArrayInputStream bais = new ByteArrayInputStream(mts.getStream()); dom = new SAXBuilder().build(bais); bais.close(); } catch (JDOMException e) { throw new IllegalRepositoryStateException("Cannot parse MD_EVENTS: " + pid, e); } catch (IOException e) { throw new Error(e); } catch (NotFoundException e) { log.warn("Could not find MD_EVENTS for {}, creating a new document", pid); dom = new Document(); Element premis = new Element("premis", JDOMNamespaceUtil.PREMIS_V2_NS).addContent(PremisEventLogger.getObjectElement(pid)); dom.setRootElement(premis); newDatastream = true; } eventLogger.appendLogEvents(pid, dom.getRootElement()); String eventsLoc = this.upload(dom); String logTimestamp; if (newDatastream) { logTimestamp = this.addManagedDatastream(pid, MD_EVENTS.getName(), false, "adding PREMIS events", new ArrayList<String>(), MD_EVENTS.getLabel(), MD_EVENTS.isVersionable(), "text/xml", eventsLoc); } else { logTimestamp = this.modifyDatastreamByReference(pid, MD_EVENTS.getName(), false, "adding PREMIS events", new ArrayList<String>(), MD_EVENTS.getLabel(), "text/xml", null, null, eventsLoc); } return logTimestamp; } public void writePremisEventsToFedoraObject(final PremisEventLogger eventLogger, final Collection<PID> pids) { final AccessGroupSet groups = GroupsThreadStore.getGroups(); Runnable premisRunnable = new Runnable() { @Override public void run() { try { GroupsThreadStore.storeGroups(groups); for (PID pid : pids) { try { writePremisEventsToFedoraObject(eventLogger, pid); } catch (FedoraException e) { log.error("Failed to update premis for {}", pid, e); } } } finally { GroupsThreadStore.clearGroups(); } } }; Thread thread = new Thread(premisRunnable); thread.start(); } /** * Returns response containing the jdom document representing the datastream and the last modified date. If it does * not exist, then null is returned. If the document cannot be parsed, a ServiceException is thrown. * * @param pid * @param datastreamName * @return * @throws FedoraException */ public DatastreamDocument getXMLDatastreamIfExists(PID pid, String datastreamName) throws FedoraException { log.debug("Attempting to get datastream " + datastreamName + " for object " + pid); try { while (true) { edu.unc.lib.dl.fedora.types.Datastream datastream = this.getDatastream(pid, datastreamName); if (datastream == null) { return null; } log.debug("Got datastream, attempting to get dissemination version with create date " + datastream.getCreateDate()); try { MIMETypedStream mts = accessClient.getDatastreamDissemination(pid, datastreamName, datastream.getCreateDate()); try (ByteArrayInputStream bais = new ByteArrayInputStream(mts.getStream())) { Document dsDoc = new SAXBuilder().build(bais); return new DatastreamDocument(dsDoc, datastream.getCreateDate()); } catch (JDOMException | IOException e) { throw new ServiceException("Failed to parse datastream " + datastreamName + " for object " + pid, e); } } catch (NotFoundException e) { log.debug("No dissemination version for create date " + datastream.getCreateDate() + " found, retrying"); } } } catch (NotFoundException e) { return null; } } public DatastreamDocument getRELSEXTWithRetries(PID pid) throws FedoraException { for (int tries = RELS_EXT_RETRIES; tries > 0; tries--) { DatastreamDocument relsExtResp = getXMLDatastreamIfExists(pid, RELS_EXT.getName()); if (relsExtResp == null) { log.debug("Could not find RELS-EXT for {}, retrying", pid); try { Thread.sleep(RELS_EXT_RETRY_DELAY); } catch (InterruptedException e) { break; } } else { return relsExtResp; } } throw new NotFoundException("Unable to retrieve RELS-EXT for " + pid); } public void setExclusiveTripleRelation(PID pid, String predicate, Namespace namespace, PID exclusivePID) throws FedoraException{ setExclusiveTriple(pid, predicate, namespace, exclusivePID.toString(), false, null); } public void setExclusiveLiteral(PID pid, String predicate, Namespace namespace, String newExclusiveValue, String datatype) throws FedoraException { setExclusiveTriple(pid, predicate, namespace, newExclusiveValue, true, datatype); } private void setExclusiveTriple(PID pid, String predicate, Namespace namespace, String value, boolean isLiteral, String datatype) throws FedoraException { do { DatastreamDocument dsDoc = getRELSEXTWithRetries(pid); try { Document doc = dsDoc.getDocument(); RDFXMLUtil.setExclusiveTriple(doc.getRootElement(), predicate, namespace, isLiteral, value, datatype); modifyDatastream(pid, RELS_EXT.getName(), "Setting exclusive relation", dsDoc.getLastModified(), dsDoc.getDocument()); return; } catch (OptimisticLockException e) { log.debug("Unable to update RELS-EXT for {}, retrying", pid, e); } } while (true); } // @Override // public Object sendAndReceive(String uriString, WebServiceMessageCallback requestCallback, // WebServiceMessageExtractor responseExtractor) { // Assert.notNull(responseExtractor, "'responseExtractor' must not be null"); // Assert.hasLength(uriString, "'uri' must not be empty"); // TransportContext previousTransportContext = TransportContextHolder.getTransportContext(); // WebServiceConnection connection = null; // try { // connection = createConnection(URI.create(uriString)); // if(connection instanceof CommonsHttpConnection) { // CommonsHttpConnection commonsConn = (CommonsHttpConnection)connection; // commonsConn.getPostMethod().addRequestHeader("myGroupsHeader", "someshibbolethgroups"); // } // TransportContextHolder.setTransportContext(new DefaultTransportContext(connection)); // MessageContext messageContext = new DefaultMessageContext(getMessageFactory()); // // return doSendAndReceive(messageContext, connection, requestCallback, responseExtractor); // } catch (TransportException ex) { // throw new WebServiceTransportException("Could not use transport: " + ex.getMessage(), ex); // } catch (IOException ex) { // throw new WebServiceIOException("I/O error: " + ex.getMessage(), ex); // } finally { // TransportUtils.closeConnection(connection); // TransportContextHolder.setTransportContext(previousTransportContext); // } // } /** * @param accessClient * the accessClient to set */ public void setAccessClient(AccessClient accessClient) { this.accessClient = accessClient; } /** * @return the accessClient */ public AccessClient getAccessClient() { return accessClient; } public String getFedoraHost() { return fedoraHost; } public void setFedoraHost(String fedoraHost) { this.fedoraHost = fedoraHost; } }