package dk.statsbiblioteket.medieplatform.autonomous; import dk.statsbiblioteket.autonomous.premis.AgentComplexType; import dk.statsbiblioteket.autonomous.premis.AgentIdentifierComplexType; import dk.statsbiblioteket.autonomous.premis.EventComplexType; import dk.statsbiblioteket.autonomous.premis.EventIdentifierComplexType; import dk.statsbiblioteket.autonomous.premis.EventOutcomeDetailComplexType; import dk.statsbiblioteket.autonomous.premis.EventOutcomeInformationComplexType; import dk.statsbiblioteket.autonomous.premis.LinkingAgentIdentifierComplexType; import dk.statsbiblioteket.autonomous.premis.LinkingObjectIdentifierComplexType; import dk.statsbiblioteket.autonomous.premis.ObjectComplexType; import dk.statsbiblioteket.autonomous.premis.ObjectFactory; import dk.statsbiblioteket.autonomous.premis.ObjectIdentifierComplexType; import dk.statsbiblioteket.autonomous.premis.PremisComplexType; import dk.statsbiblioteket.autonomous.premis.Representation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.namespace.QName; import java.io.InputStream; import java.io.StringWriter; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Class for transforming the premis structure. Contains methods for creating the premis from scratch, and for adding * events. * The class is not thread safe, so do not use it as such */ public class PremisManipulator<T extends Item> { private final static QName _EventOutcome_QNAME = new QName("info:lc/xmlns/premis-v2", "eventOutcome"); private final static QName _EventOutcomeDetailNote_QNAME = new QName( "info:lc/xmlns/premis-v2", "eventOutcomeDetailNote"); private final static QName _EventOutcomeDetail_QNAME = new QName("info:lc/xmlns/premis-v2", "eventOutcomeDetail"); //TODO logging in all the methods private static Logger log = LoggerFactory.getLogger(PremisManipulator.class); private final PremisComplexType premis; private Marshaller marshaller; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); private final SimpleDateFormat legacyDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZ"); private final String type; private final ItemFactory<T> itemFactory; public PremisManipulator(InputStream premis, String type, JAXBContext context, ItemFactory<T> itemFactory) throws JAXBException { this.type = type; this.itemFactory = itemFactory; this.marshaller = createMarshaller(context); this.premis = ((JAXBElement<PremisComplexType>) context.createUnmarshaller().unmarshal(premis)).getValue(); } private static Marshaller createMarshaller(JAXBContext context) throws JAXBException { Marshaller temp = context.createMarshaller(); temp.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); return temp; } public PremisManipulator(String itemID, String type, JAXBContext context, ItemFactory<T> itemFactory) throws JAXBException { this.itemFactory = itemFactory; this.marshaller = createMarshaller(context); premis = new ObjectFactory().createPremisComplexType(); premis.setVersion("2.2"); this.type = type; addObjectIfNessesary(premis.getObject(), itemID); } /** * Make this Premis as a Batch. The Event class is poorer than the Premis events, so the "agent" information * will not be included * * @return the blob as a Batch */ public T toItem() { T item = itemFactory.create(getObjectID()); item.setEventList(getEvents()); return item; } /** * Get the id of the object object in premis * * @return the object */ private String getObjectID() { Representation object = (Representation) premis.getObject().get(0); return object.getObjectIdentifier().get(0).getObjectIdentifierValue(); } /** * convert the events to a list of Events * * @return the Events */ private List<Event> getEvents() { List<EventComplexType> premisEvents = premis.getEvent(); List<Event> result = new ArrayList<>(premisEvents.size()); for (EventComplexType premisEvent : premisEvents) { result.add(convert(premisEvent)); } return result; } /** * Convert one event to a Event * * @param premisEvent the event * * @return the Event */ private Event convert(EventComplexType premisEvent) { Event result = new Event(); result.setEventID(premisEvent.getEventType()); result.setDetails(premisEvent.getEventDetail()); try { result.setDate(dateFormat.parse(premisEvent.getEventDateTime())); } catch (ParseException e) { try { result.setDate(legacyDateFormat.parse(premisEvent.getEventDateTime())); } catch(ParseException e2){ //no date is set, then log.warn("Failed to parse Premis event {} date {} (may not be set)", result.getEventID(), premisEvent.getEventDateTime()); } } EventOutcomeInformationComplexType eventOutcomeInformation = premisEvent.getEventOutcomeInformation().get(0); for (JAXBElement<?> jaxbElement : eventOutcomeInformation.getContent()) { if (jaxbElement.getName().equals(_EventOutcome_QNAME)) { String value = jaxbElement.getValue().toString(); if (value.equals("success")) { result.setSuccess(true); } } if (jaxbElement.getName().equals(_EventOutcomeDetail_QNAME)) { EventOutcomeDetailComplexType detailBlobs = (EventOutcomeDetailComplexType) jaxbElement.getValue(); String details = detailBlobs.getEventOutcomeDetailNote(); if (details != null) { result.setDetails(details); } } } return result; } /** * Add an event to the premis blob. Will return itself to allow for method chaining. * * @param agent the agent that did it * @param timestamp when the thing was done * @param details details about how it went * @param eventType the kind of thing that was done * @param outcome was it successful? * * @return the premis with the event added. */ public PremisManipulator<T> appendEvent(String agent, Date timestamp, String details, String eventType, boolean outcome) { String eventID = constructEventID(timestamp); if (eventExists(eventID)) { return this; } EventComplexType event = createEvent(agent, timestamp, details, eventType, outcome); premis.getEvent().add(event); return this; } /** * Add an event to the head of the premis blob. Will return itself to allow for method chaining. * * @param agent the agent that did it * @param timestamp when the thing was done * @param details details about how it went * @param eventType the kind of thing that was done * @param outcome was it successful? * * @return the premis with the event added to head. */ public PremisManipulator<T> prependEvent(String agent, Date timestamp, String details, String eventType, boolean outcome) { String eventID = constructEventID(timestamp); if (eventExists(eventID)) { return this; } EventComplexType event = createEvent(agent, timestamp, details, eventType, outcome); premis.getEvent().add(0, event); return this; } private EventComplexType createEvent(String agent, Date timestamp, String details, String eventType, boolean outcome) { String eventID = constructEventID(timestamp); addAgentIfNessesary(premis.getAgent(), agent); ObjectFactory factory = new ObjectFactory(); EventComplexType event = factory.createEventComplexType(); event.setEventDateTime(dateFormat.format(timestamp)); event.setEventType(eventType); EventIdentifierComplexType identifier = factory.createEventIdentifierComplexType(); identifier.setEventIdentifierType(type); identifier.setEventIdentifierValue(eventID); event.setEventIdentifier(identifier); EventOutcomeInformationComplexType outcomeObject = factory.createEventOutcomeInformationComplexType(); String outcomeString = (outcome ? "success" : "failure"); outcomeObject.getContent().add(factory.createEventOutcome(outcomeString)); EventOutcomeDetailComplexType eventOutcomeDetail = factory.createEventOutcomeDetailComplexType(); eventOutcomeDetail.setEventOutcomeDetailNote(details); outcomeObject.getContent().add(factory.createEventOutcomeDetail(eventOutcomeDetail)); event.getEventOutcomeInformation().add(outcomeObject); LinkingAgentIdentifierComplexType linkingAgentObject = factory.createLinkingAgentIdentifierComplexType(); linkingAgentObject.setLinkingAgentIdentifierType(type); linkingAgentObject.setLinkingAgentIdentifierValue(agent); event.getLinkingAgentIdentifier().add(linkingAgentObject); LinkingObjectIdentifierComplexType linkingObjectObject = factory.createLinkingObjectIdentifierComplexType(); linkingObjectObject.setLinkingObjectIdentifierType(type); linkingObjectObject.setLinkingObjectIdentifierValue(getObjectID()); event.getLinkingObjectIdentifier().add(linkingObjectObject); return event; } /** * Remove all events from the PREMIS blob with date time later-than-or-equal to the earliest failure. This method * does not remove the corresponding agent (if any) from the PREMIS blob so there could be agents left which have * no referrent. There is no reason to believe that this is a problem. * * @param eventId the earliest event to remove. If null, find the earliest failure instead. * * @return the number of events removed, or the number which would have been removed if doit=false */ public int removeEventsFromFailureOrEvent(String eventId) { List<EventComplexType> premisEvents = premis.getEvent(); List<EventComplexType> eventsToRemove = findEventsAfterThisEvent(premisEvents, eventId); premisEvents.removeAll(eventsToRemove); return eventsToRemove.size(); } /** * Find events to remove. * * @param premisEvents the complete events list. * @param eventId the earliest event to remove. If null, find the earliest failure instead. * * @return the list of events to remove. */ private List<EventComplexType> findEventsAfterThisEvent(List<EventComplexType> premisEvents, String eventId) { Date earliestEventToRemove = null; if (eventId == null) { earliestEventToRemove = findDateOfEarliestFailure(premisEvents); } else { earliestEventToRemove = findDateOfGivenEvent(premisEvents, eventId); } List<EventComplexType> eventsToRemove = new ArrayList<EventComplexType>(); if (earliestEventToRemove != null) { for (EventComplexType premisEvent : premisEvents) { Event event = convert(premisEvent); if (event.getDate().compareTo(earliestEventToRemove) >= 0) { eventsToRemove.add(premisEvent); } } } return eventsToRemove; } private Date findDateOfEarliestFailure(List<EventComplexType> premisEvents) { Date earliestFailure = null; for (EventComplexType premisEvent : premisEvents) { Event event = convert(premisEvent); if (!event.isSuccess()) { if (earliestFailure == null || event.getDate().before(earliestFailure)) { earliestFailure = event.getDate(); } } } return earliestFailure; } private Date findDateOfGivenEvent(List<EventComplexType> premisEvents, String eventId) { for (EventComplexType premisEvent : premisEvents) { Event event = convert(premisEvent); if (event.getEventID().equals(eventId)) { return event.getDate(); } } return null; } /** * Returns true, if an event with the same identifier in the same type exists * * @param eventID the event id to search for * * @return true if found */ private boolean eventExists(String eventID) { for (EventComplexType event : premis.getEvent()) { if (event.getEventIdentifier().getEventIdentifierType().equals(type) && event.getEventIdentifier() .getEventIdentifierValue() .equals(eventID)) { return true; } } return false; } private String constructEventID(Date timestamp) { return getObjectID() + "-" + String.valueOf(timestamp.getTime()); } /** * Get the premis as xml * * @return the premis as xml */ public String toXML() { try { StringWriter writer = new StringWriter(); marshaller.marshal(new ObjectFactory().createPremis(premis), writer); return writer.toString(); } catch (JAXBException e) { log.error("Failed to serialize premis as Xml",e); return null; } } /** * Add the objectList * * @param objectList the list to add it to * @param fullID the full id of the objectList */ private void addObjectIfNessesary(List<ObjectComplexType> objectList, String fullID) { if (objectList.size() == 0) { ObjectFactory factory = new ObjectFactory(); Representation representation = factory.createRepresentation(); ObjectIdentifierComplexType objectIdentifier = factory.createObjectIdentifierComplexType(); objectIdentifier.setObjectIdentifierType(type); objectIdentifier.setObjectIdentifierValue(fullID); representation.getObjectIdentifier().add(objectIdentifier); objectList.add(representation); } } /** * Add the agentList, if it is not there already * * @param agentList the agent list to add the agent to * @param agent1 the agent name */ private void addAgentIfNessesary(List<AgentComplexType> agentList, String agent1) { ObjectFactory factory = new ObjectFactory(); for (AgentComplexType agentComplexType : agentList) { for (AgentIdentifierComplexType agentIdentifierComplexType : agentComplexType.getAgentIdentifier()) { if (agentIdentifierComplexType.getAgentIdentifierValue().equals(agent1)) { return; } } } AgentIdentifierComplexType identifier = factory.createAgentIdentifierComplexType(); identifier.setAgentIdentifierValue(agent1); identifier.setAgentIdentifierType(type); AgentComplexType agentCreated = factory.createAgentComplexType(); agentCreated.getAgentIdentifier().add(identifier); agentList.add(agentCreated); } public int removeEvents(String eventId) { List<EventComplexType> premisEvents = premis.getEvent(); List<EventComplexType> eventsToRemove = findEventsWithThisID(premisEvents, eventId); premisEvents.removeAll(eventsToRemove); return eventsToRemove.size(); } /** * Find events to remove. * * @param premisEvents the complete events list. * @param eventType the earliest event to remove. If null, find the earliest failure instead. * * @return the list of events to remove. */ private List<EventComplexType> findEventsWithThisID(List<EventComplexType> premisEvents, String eventType) { List<EventComplexType> eventsToRemove = new ArrayList<>(); for (EventComplexType premisEvent : premisEvents) { if (eventType.equals(premisEvent.getEventType())){ eventsToRemove.add(premisEvent); } } return eventsToRemove; } }