/* * Copyright (C) 2013 Jan Pokorsky * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package cz.cas.lib.proarc.common.device; import com.yourmediashelf.fedora.client.FedoraClientException; import com.yourmediashelf.fedora.generated.management.DatastreamProfile; import cz.cas.lib.proarc.common.dublincore.DcStreamEditor; import cz.cas.lib.proarc.common.dublincore.DcStreamEditor.DublinCoreRecord; import cz.cas.lib.proarc.common.fedora.DigitalObjectConcurrentModificationException; import cz.cas.lib.proarc.common.fedora.DigitalObjectException; import cz.cas.lib.proarc.common.fedora.DigitalObjectNotFoundException; import cz.cas.lib.proarc.common.fedora.FedoraObject; import cz.cas.lib.proarc.common.fedora.FoxmlUtils; import cz.cas.lib.proarc.common.fedora.LocalStorage; import cz.cas.lib.proarc.common.fedora.LocalStorage.LocalObject; import cz.cas.lib.proarc.common.fedora.RemoteStorage; import cz.cas.lib.proarc.common.fedora.RemoteStorage.RemoteObject; import cz.cas.lib.proarc.common.fedora.SearchView.Item; import cz.cas.lib.proarc.common.fedora.XmlStreamEditor; import cz.cas.lib.proarc.common.fedora.XmlStreamEditor.EditorResult; import cz.cas.lib.proarc.common.fedora.relation.RelationEditor; import cz.cas.lib.proarc.mix.Mix; import cz.cas.lib.proarc.mix.MixUtils; import cz.cas.lib.proarc.oaidublincore.ElementType; import cz.cas.lib.proarc.oaidublincore.OaiDcType; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import javax.ws.rs.core.Response.Status; import javax.xml.transform.Source; /** * The repository of devices producing digital objects. * * @author Jan Pokorsky */ public final class DeviceRepository { /** * The datastream ID. It holds the device description in XML format. */ public static final String DESCRIPTION_DS_ID = "DESCRIPTION"; /** * The default label of {@link #DESCRIPTION_DS_ID the description datastream}. */ public static final String DESCRIPTION_DS_LABEL = "The device description"; /** * PID of the device model. */ public static final String METAMODEL_ID = "proarc:device"; private static final String DEVICE_ID_PREFIX = "device:"; private final RemoteStorage remoteStorage; public DeviceRepository(RemoteStorage remoteStorage) { if (remoteStorage == null) { throw new NullPointerException("remoteStorage"); } this.remoteStorage = remoteStorage; } /** * Adds a new device. * @param owner owner of the object * @param label device label * @param log log message * @return the device * @throws DeviceException failure */ public Device addDevice(String owner, String label, String log) throws DeviceException { UUID uuid = UUID.randomUUID(); String pid = DEVICE_ID_PREFIX + uuid.toString(); try { return addDevice(pid, owner, label, log); } catch (DigitalObjectException ex) { throw new DeviceException(pid, ex); } } /** * Deletes a device. * @param id device PID * @param log log message * @return {@code true} if deleted or {@code false} if the device is connected by any digital object. * @throws DeviceException failure * @throws DeviceNotFoundException device not found */ public boolean deleteDevice(String id, String log) throws DeviceException, DeviceNotFoundException { checkDeviceId(id); RemoteObject robject = remoteStorage.find(id); try { // check fedora usages // device may be still used by any import item if (remoteStorage.getSearch().isDeviceInUse(id)) { return false; } else { robject.purge(log); return true; } } catch (DigitalObjectNotFoundException ex) { throw new DeviceNotFoundException(null, ex, id); } catch (FedoraClientException ex) { if (ex.getStatus() == Status.NOT_FOUND.getStatusCode()) { throw new DeviceNotFoundException(null, ex, id); } else { throw new DeviceException(id, ex); } } catch (DigitalObjectException | IOException ex) { throw new DeviceException(id, ex); } } /** * Finds a device without description. * @param id device PID or {@code null} for all devices. * @return list of devices * @throws DeviceException failure */ public List<Device> find(String id) throws DeviceException { return find(id, false); } /** * Finds a device. * @param id device PID or {@code null} for all devices. * @param fetchDescription whether to include device descriptions in response * @return list of devices * @throws DeviceException failure */ public List<Device> find(String id, boolean fetchDescription) throws DeviceException { try { List<Device> devices; if (id != null) { checkDeviceId(id); devices = findDevice(id); } else { devices = findAllDevices(); } if (fetchDescription) { fetchDeviceDescription(devices); } return devices; } catch (IOException ex) { throw new DeviceException(id, ex); } catch (FedoraClientException ex) { throw new DeviceException(id, ex); } } /** * Fetches device descriptions. * @param devices devices to query * @throws DeviceException failure */ void fetchDeviceDescription(List<Device> devices) throws DeviceException { for (Device device : devices) { fetchDeviceDescription(device); } } /** * Fetches a device description. * @param device a device with ID * @return the device or {@code null} if not found * @throws DeviceException failure */ Device fetchDeviceDescription(Device device) throws DeviceException { if (device == null || device.getId() == null) { return null; } String id = device.getId(); try { RemoteObject robj = remoteStorage.find(id); XmlStreamEditor editor = getMixDescriptionEditor(robj); Source src = editor.read(); Mix desc; if (src != null) { desc = MixUtils.unmarshal(src, Mix.class); } else { desc = new Mix(); } device.setDescription(desc); device.setTimestamp(editor.getLastModified()); return device; } catch (DigitalObjectNotFoundException ex) { return null; } catch (DigitalObjectException ex) { throw new DeviceException(id, ex); } } /** * Updates a device. * @param update data to update * @param log log message * @return the updated device * @throws DeviceException failure */ public Device update(Device update, String log) throws DeviceException { String id = update.getId(); String label = update.getLabel(); checkDeviceId(id); try { RemoteObject robj = remoteStorage.find(id); updateDc(robj, id, label, log); XmlStreamEditor descriptionEditor = getMixDescriptionEditor(robj); Source oldDescSrc = descriptionEditor.read(); if (oldDescSrc == null) { update.setTimestamp(descriptionEditor.getLastModified()); } if (oldDescSrc != null && update.getDescription() == null) { update.setDescription(new Mix()); } if (update.getDescription() != null) { EditorResult result = descriptionEditor.createResult(); MixUtils.marshal(result, update.getDescription(), true); descriptionEditor.write(result, update.getTimestamp(), log); } robj.setLabel(label); robj.flush(); Device device = new Device(); device.setId(id); device.setLabel(label); device.setDescription(update.getDescription()); device.setTimestamp(descriptionEditor.getLastModified()); return device; } catch (DigitalObjectConcurrentModificationException ex) { // XXX handle concurrency throw new DeviceException(id, ex); } catch (DigitalObjectException ex) { throw new DeviceException(id, ex); } } private Device addDevice(String pid, String owner, String label, String log) throws DigitalObjectException { LocalObject lobject = new LocalStorage().create(pid); lobject.setLabel(label); lobject.setOwner(owner); updateDc(lobject, pid, label, log); RelationEditor relationEditor = new RelationEditor(lobject); relationEditor.setModel(METAMODEL_ID); relationEditor.write(relationEditor.getLastModified(), log); lobject.flush(); remoteStorage.ingest(lobject, owner); Device device = new Device(); device.setId(pid); device.setLabel(label); return device; } private List<Device> findAllDevices() throws IOException, FedoraClientException { List<Item> items = remoteStorage.getSearch().findByModel(METAMODEL_ID); return objectAsDevice(items, null); } private List<Device> findDevice(String... pids) throws IOException, FedoraClientException { List<Item> items = remoteStorage.getSearch().findByModel(METAMODEL_ID); return objectAsDevice(items, new HashSet<String>(Arrays.asList(pids))); } private List<Device> objectAsDevice(List<Item> items, Set<String> includes) { ArrayList<Device> devices = new ArrayList<Device>(items.size()); for (Item item : items) { String label = item.getLabel(); String pid = item.getPid(); if (includes == null || includes.contains(pid)) { Device device = new Device(); device.setId(pid); device.setLabel(label); devices.add(device); } } return devices; } private void updateDc(FedoraObject robj, String id, String label, String log) throws DigitalObjectException { DcStreamEditor dcEditor = new DcStreamEditor(robj); DublinCoreRecord dcr = dcEditor.read(); OaiDcType dc = new OaiDcType(); dc.getTitles().add(new ElementType(label, null)); dc.getIdentifiers().add(new ElementType(id, null)); dc.getTypes().add(new ElementType(METAMODEL_ID, null)); dcr.setDc(dc); dcEditor.write(dcr, log); } static void checkDeviceId(String id) throws DeviceException { if (id == null || !id.startsWith(DEVICE_ID_PREFIX)) { throw new DeviceException("Unexpected device ID: " + id); } } /** * Gets a datastream editor for MIX format. * @param robj an object to edit * @return the editor */ public static XmlStreamEditor getMixDescriptionEditor(FedoraObject robj) { DatastreamProfile dProfile = FoxmlUtils.managedProfile( DESCRIPTION_DS_ID, MixUtils.NS, DESCRIPTION_DS_LABEL); XmlStreamEditor editor = robj.getEditor(dProfile); return editor; } /** * Gets a datastream editor for MIX format. * * @param id * * @return * @throws DeviceException */ public XmlStreamEditor getDescriptionEditor(String id) throws DeviceException { checkDeviceId(id); RemoteObject robj = remoteStorage.find(id); return getMixDescriptionEditor(robj); } }