package dk.statsbiblioteket.medieplatform.autonomous; import dk.statsbiblioteket.doms.central.connectors.BackendInvalidCredsException; import dk.statsbiblioteket.doms.central.connectors.BackendInvalidResourceException; import dk.statsbiblioteket.doms.central.connectors.BackendMethodFailedException; import dk.statsbiblioteket.doms.central.connectors.EnhancedFedora; import dk.statsbiblioteket.doms.central.connectors.fedora.structures.ObjectProfile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.bind.JAXBException; import java.io.ByteArrayInputStream; import java.io.UnsupportedEncodingException; import java.util.ConcurrentModificationException; import java.util.Date; import java.util.List; import java.util.regex.Pattern; /** * Access to DOMS batch and event storage using the Central Webservice library to communicate with DOMS. * Implements the {@link EventStorer} interface. */ public class DomsEventStorage<T extends Item> implements EventStorer<T> { private static Logger log = LoggerFactory.getLogger(DomsEventStorage.class); protected final EnhancedFedora fedora; protected final String eventsDatastream; protected final PremisManipulatorFactory<T> premisFactory; public static final String addEventToItemComment = "Adding event to Item"; public static final String removeEventFromItemComment = "Removing event from item: "; DomsEventStorage(EnhancedFedora fedora, String type, String eventsDatastream, ItemFactory<T> itemFactory) throws JAXBException { this.fedora = fedora; this.eventsDatastream = eventsDatastream; premisFactory = new PremisManipulatorFactory<>(type, itemFactory); } @Override public Date appendEventToItem(T item, String agent, Date timestamp, String details, String eventType, boolean outcome) throws CommunicationException, NotFoundException { PremisManipulator<T> premisObject = getPremisForItem(item); premisObject = premisObject.appendEvent(agent, timestamp, details, eventType, outcome); try{ try { return fedora.modifyDatastreamByValue(item.getDomsID(), eventsDatastream, null, null, premisObject.toXML().getBytes(), null, "text/xml", addEventToItemComment, null); } catch (ConcurrentModificationException | BackendMethodFailedException | BackendInvalidCredsException e) { throw new CommunicationException("Failed appending event to item '" + item + "'", e); } } catch (BackendInvalidResourceException e1) { //But I just created the object, it must be there throw new NotFoundException("Failed appending event to item '" + item + "'", e1); } } @Override public Date prependEventToItem(T item, String agent, Date timestamp, String details, String eventType, boolean outcome) throws CommunicationException, NotFoundException { PremisManipulator<T> premisObject = getPremisForItem(item); premisObject = premisObject.prependEvent(agent, timestamp, details, eventType, outcome); try { try { return fedora.modifyDatastreamByValue(item.getDomsID(), eventsDatastream, null, null, premisObject.toXML().getBytes(), null, "text/xml", addEventToItemComment, null); } catch (ConcurrentModificationException | BackendMethodFailedException | BackendInvalidCredsException e) { throw new CommunicationException("Failed prepending event to item '" + item + "'", e); } } catch (BackendInvalidResourceException e1) { //But I just created the object, it must be there throw new NotFoundException("Failed prepending event to item '" + item + "'", e1); } } private PremisManipulator<T> getPremisForItem(T item) throws CommunicationException, NotFoundException { try { String itemID = item.getDomsID(); if (itemID == null){ itemID = getPidFromDCIdentifier(item.getFullID()); item.setDomsID(itemID); } PremisManipulator<T> premisObject; try { String premisPreBlob = fedora.getXMLDatastreamContents(itemID, eventsDatastream, null); premisObject = premisFactory.createFromBlob(new ByteArrayInputStream(premisPreBlob.getBytes())); } catch (BackendInvalidResourceException e) { //okay, no EVENTS datastream premisObject = premisFactory.createInitialPremisBlob(item.getFullID()); } return premisObject; } catch (BackendMethodFailedException | BackendInvalidCredsException | JAXBException e) { throw new CommunicationException(e); } } /** * Removes all instances of events with the given type from the item * @param item The item to remove events from * @param eventType The eventType to remove * * @return the number of events removed * @throws CommunicationException * @throws NotFoundException */ @Override public int removeEventFromItem(T item, String eventType) throws CommunicationException, NotFoundException { try { String itemID = item.getDomsID(); if (itemID == null) { itemID = getPidFromDCIdentifier(item.getFullID()); } PremisManipulator<T> premisObject; String premisPreBlob = fedora.getXMLDatastreamContents(itemID, eventsDatastream, null); premisObject = premisFactory.createFromBlob(new ByteArrayInputStream(premisPreBlob.getBytes())); int removed = premisObject.removeEvents(eventType); if (removed > 0) { fedora.modifyDatastreamByValue(itemID, eventsDatastream, null, null, premisObject.toXML().getBytes(), null, "text/xml", removeEventFromItemComment + eventType, null); } return removed; } catch (BackendMethodFailedException | BackendInvalidCredsException | JAXBException e) { throw new CommunicationException(e); } catch (BackendInvalidResourceException e){ throw new NotFoundException(e); } } public T getItemFromFullID(String itemFullID) throws CommunicationException, NotFoundException { String roundTripID = getPidFromDCIdentifier(itemFullID); return getItemFromDomsID(roundTripID); } /** * Retrieve an Item. If the object exist in DOMS, but have no EVENTS datastream, create the ITEM with a blank * Premis blob, with the domsId as object id. * * @param domsId the id of the item object in doms * * @return the Item * @throws NotFoundException if the Item object is not found * @throws CommunicationException if communication with doms failed */ public T getItemFromDomsID(String domsId) throws CommunicationException, NotFoundException { PremisManipulator<T> premisObject; try { try { String premisPreBlob = fedora.getXMLDatastreamContents(domsId, eventsDatastream, null); premisObject = premisFactory.createFromBlob(new ByteArrayInputStream(premisPreBlob.getBytes())); } catch (BackendInvalidResourceException e) { //This could be a missing datastream or object try { ObjectProfile profile = fedora.getObjectProfile(domsId, null); //Get profile to check that the obejct is there premisObject = premisFactory.createInitialPremisBlob(domsId); //Okay, an object, create an empty premis block } catch (BackendInvalidResourceException e1) { //Not event the object throw new NotFoundException(e1); } } T item = premisObject.toItem(); item.setDomsID(domsId); return item; } catch (BackendMethodFailedException | BackendInvalidCredsException | JAXBException e) { throw new CommunicationException(e); } } @Override public int triggerWorkflowRestartFromFirstFailure(T item) throws CommunicationException, NotFoundException { return triggerWorkflowRestartFromFirstFailure(item, null); } /** * This method carries out a single attempt to restart the workflow from where it first failed. * * @param eventId the first event to remove or null if all events after the first failure are to be * removed. * * @return the number of events removed or -1 of there was a ConcurrentModificationException thrown. * @throws CommunicationException if there was a problem communicating with DOMS. */ @Override public int triggerWorkflowRestartFromFirstFailure(T item, String eventId) throws CommunicationException, NotFoundException { String itemPid = item.getDomsID(); if (itemPid == null) { itemPid = getPidFromDCIdentifier(item.getFullID()); } try { Date lastModifiedDate = fedora.getObjectProfile(itemPid, null).getObjectLastModifiedDate(); String premisPreBlob = fedora.getXMLDatastreamContents(itemPid, eventsDatastream, null); PremisManipulator<T> premisObject = premisFactory.createFromBlob(new ByteArrayInputStream(premisPreBlob.getBytes())); int eventsRemoved = premisObject.removeEventsFromFailureOrEvent(eventId); if (eventsRemoved > 0) { //backupEventsForBatch(batchId, roundTripNumber); try { fedora.modifyDatastreamByValue( itemPid, eventsDatastream, null, null, premisObject.toXML().getBytes("UTF-8"), null, "text/xml", "Event list trimmed of all events after earliest failure", lastModifiedDate.getTime()); } catch (ConcurrentModificationException e) { log.warn( "Failed to trigger restart of batch round trip for " + item.getFullID() + " on this attempt. Another process modified the object concurrently." ); return -1; } catch (UnsupportedEncodingException e) { throw new Error("UTF-8 not supported.", e); } } return eventsRemoved; } catch (BackendInvalidResourceException | JAXBException | BackendInvalidCredsException | BackendMethodFailedException e) { throw new CommunicationException(e); } } /** * Retrieve the corresponding doms pid of the object with this dc identifier * * @return the doms pid * @throws CommunicationException failed to communicate * @throws BackendInvalidResourceException object not found */ String getPidFromDCIdentifier(String fullID) throws CommunicationException, NotFoundException { try { final String dcIdentifier = toDCIdentifier(fullID); List<String> founds = fedora.findObjectFromDCIdentifier(dcIdentifier); if (founds.size() > 0) { return founds.get(0); } throw new NotFoundException("Doms Object not found for dc identifier " + dcIdentifier); } catch (BackendMethodFailedException | BackendInvalidCredsException e) { throw new CommunicationException(e); } } public static String toDCIdentifier(String fullID) { if (!fullID.startsWith("path:")) { return String.format("path:%s", fullID); } return fullID; } }