/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2011-2015 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4chee.archive.hsm.impl; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.ejb.Stateless; import javax.enterprise.event.Event; import javax.inject.Inject; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.dcm4che3.data.Attributes; import org.dcm4che3.net.Device; import org.dcm4che3.util.AttributesFormat; import org.dcm4chee.archive.entity.Instance; import org.dcm4chee.archive.entity.Location; import org.dcm4chee.archive.entity.Location.Status; import org.dcm4chee.archive.entity.Study; import org.dcm4chee.archive.entity.Utils; import org.dcm4chee.archive.hsm.LocationCopyContext; import org.dcm4chee.archive.hsm.LocationsCopied; import org.dcm4chee.archive.locationmgmt.LocationMgmt; import org.dcm4chee.storage.ContainerEntry; import org.dcm4chee.storage.archiver.service.ArchiverContext; import org.dcm4chee.storage.archiver.service.ArchiverService; import org.dcm4chee.storage.conf.StorageDeviceExtension; import org.dcm4chee.storage.conf.StorageSystemGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Gunter Zeilinger <gunterze@gmail.com> * @author Franz Willer <franz.willer@gmail.com> * @author Steve Kroetsch <stevekroetsch@hotmail.com> * */ @Stateless public class LocationCopyServiceEJB { private static final Logger LOG = LoggerFactory.getLogger(LocationCopyServiceEJB.class); private static final String INSTANCE_PK = "instance_pk"; private static final String DIGEST = "digest"; private static final String OTHER_ATTRS_DIGEST = "otherAttrsDigest"; private static final String FILE_SIZE = "fileSize"; private static final String TRANSFER_SYNTAX = "transferSyntax"; private static final String TIME_ZONE = "timeZone"; private static final String DELETE_SOURCE = "deleteSource"; private static final String LOCATION = "location"; private static final String SOURCE_LOCATION_PKS_TO_DELETE = "srcLocationPksToDelete"; private static final String LOCATION_COPY_CONTEXT = "locationCopyContext"; @PersistenceContext(name = "dcm4chee-arc", unitName = "dcm4chee-arc") private EntityManager em; @Inject private Device device; @Inject private ArchiverService archiverService; @Inject private LocationMgmt locationMgmt; @LocationsCopied @Inject private Event<LocationCopyContext> locationsCopied; public void scheduleSeries(LocationCopyContext ctx, String seriesIUID, long delay) throws IOException { LOG.info( "Scheduling archiving series={}, sourceStorageGroupID={}, targetStorageGroupID={}, deleteSource={}", new Object[] { seriesIUID, ctx.getSourceStorageSystemGroupID(), ctx.getTargetStorageSystemGroupID(), ctx.getDeleteSourceLocaton() }); List<Instance> insts = em .createNamedQuery(Instance.FIND_BY_SERIES_INSTANCE_UID, Instance.class) .setParameter(1, seriesIUID).getResultList(); if (insts.size() > 0) { scheduleInstances(ctx, insts, delay); LOG.info( "Scheduled archiving {} instances of series={}, sourceStorageGroupID={}, targetStorageGroupID={}, deleteSource={}", new Object[] { seriesIUID, ctx.getSourceStorageSystemGroupID(), ctx.getTargetStorageSystemGroupID(), ctx.getDeleteSourceLocaton() }); } } public void scheduleInstances(LocationCopyContext ctx, List<Instance> insts, long delay) throws IOException { Instance instance = insts.get(0); Attributes attrs = Utils.mergeAndNormalize(instance.getSeries().getStudy().getAttributes(), instance.getSeries().getAttributes(), instance.getAttributes()); String targetName = getTargetName(attrs, ctx.getTargetStorageSystemGroupID()); scheduleInstances(ctx, insts, targetName, delay); } public void scheduleInstances(LocationCopyContext ctx, List<Instance> insts, String targetName, long delay) throws IOException { List<ContainerEntry> entries = new ArrayList<ContainerEntry>(insts.size()); LocationDeleteContext deleteCtx = ctx.getDeleteSourceLocaton() ? new LocationDeleteContext( insts.size()) : null; for (Instance inst : filterInstancesAlreadyArchived(insts, ctx.getTargetStorageSystemGroupID(), ctx.getDeleteSourceLocaton())) { Location selected = (ctx.getSourceStorageSystemGroupID() == null) ? selectBestAvailableLocation(inst) : selectLocationFromStorageGroup(inst, ctx.getSourceStorageSystemGroupID()); ContainerEntry entry = new ContainerEntry.Builder(inst.getSopInstanceUID(), selected.getDigest()) .setSourceStorageSystemGroupID(selected.getStorageSystemGroupID()) .setSourceStorageSystemID(selected.getStorageSystemID()) .setSourceName(selected.getStoragePath()) .setSourceEntryName(selected.getEntryName()) .setProperty(INSTANCE_PK, inst.getPk()) .setProperty(DIGEST, selected.getDigest()) .setProperty(OTHER_ATTRS_DIGEST, selected.getOtherAttsDigest()) .setProperty(FILE_SIZE, selected.getSize()) .setProperty(TRANSFER_SYNTAX, selected.getTransferSyntaxUID()) .setProperty(TIME_ZONE, selected.getTimeZone()) .setProperty(LOCATION, selected) .build(); entries.add(entry); if (deleteCtx != null) { deleteCtx.add(selected.getPk(), inst.getPk()); } ctx.addScheduledInstancePk(inst.getPk()); } if (entries.size() > 0) { ArchiverContext archiverCtx = archiverService.createContext(archiverService, ctx.getTargetStorageSystemGroupID(), targetName); archiverCtx.setEntries(entries); archiverCtx.setProperty(DELETE_SOURCE, new Boolean(ctx.getDeleteSourceLocaton())); if (deleteCtx != null) { archiverCtx.setProperty(SOURCE_LOCATION_PKS_TO_DELETE, deleteCtx); } archiverCtx.setProperty(LOCATION_COPY_CONTEXT, ctx); archiverService.scheduleStore(archiverCtx, delay); } } public List<Instance> filterInstancesAlreadyArchived(List<Instance> insts, String targetGroupID, boolean deleteSource) { LocationDeleteContext deleteCtx = deleteSource ? new LocationDeleteContext() : null; List<Instance> filtered = new ArrayList<Instance>(); inst: for (Instance inst : insts) { for (Location location : inst.getLocations()) { if (location.getStorageSystemGroupID().equals(targetGroupID)) { LOG.info( "{} already archived to Storage System Group {} - skip from archiving", inst, targetGroupID); if (deleteCtx != null) { deleteCtx.add(location.getPk(), inst.getPk()); } continue inst; } } filtered.add(inst); } if (deleteCtx != null && deleteCtx.size() > 0) { LOG.debug("Deleting source locations for instances already archived:{}", deleteCtx.getLocationPks()); deleteLocations(deleteCtx); } return insts; } private Location selectLocationFromStorageGroup(Instance inst, String sourceGroupID) { for (Location location : inst.getLocations()) { String groupID = location.getStorageSystemGroupID(); if (groupID.equals(sourceGroupID)) { return location; } } LOG.info("{} not available at Storage System Group {} - skip from archiving", inst, sourceGroupID); return null; } private Location selectBestAvailableLocation(Instance inst) { Location selected = null; StorageSystemGroup bestGroup = null; for (Location location : inst.getLocations()) { String groupID = location.getStorageSystemGroupID(); StorageDeviceExtension stgExt = storageDeviceExtension(); StorageSystemGroup group = stgExt.getStorageSystemGroup(groupID); if (bestGroup == null || bestGroup.getStorageAccessTime() > group.getStorageAccessTime()) { bestGroup = group; selected = location; } } if (selected == null) { LOG.info("Location could not be selected for {} - skip from archiving", inst); } return selected; } private StorageDeviceExtension storageDeviceExtension() { return device.getDeviceExtension(StorageDeviceExtension.class); } private String getTargetName(Attributes attrs, String groupID) { StorageSystemGroup grp = storageDeviceExtension().getStorageSystemGroup(groupID); String pattern = grp.getStorageFilePathFormat(); return AttributesFormat.valueOf(pattern).format(attrs); } public void onContainerEntriesStored(ArchiverContext ctx) { LOG.debug("onContainerEntriesStored for {} called", ctx.getStorageSystemGroupID()); LocationCopyContext event = (LocationCopyContext) ctx.getProperty(LOCATION_COPY_CONTEXT); List<ContainerEntry> entries = ctx.getEntries(); boolean notInContainer = ctx.isNotInContainer(); for (ContainerEntry entry : entries) { Instance inst = em.find(Instance.class, entry.getProperty(INSTANCE_PK)); updateStudyAccessTime(inst, ctx.getStorageSystemGroupID()); Location location = new Location.Builder() .storageSystemGroupID(ctx.getStorageSystemGroupID()) .storageSystemID(ctx.getStorageSystemID()) .storagePath(notInContainer ? entry.getNotInContainerName() : ctx.getName()) .entryName(notInContainer ? null : entry.getName()) .digest((String) entry.getProperty(DIGEST)) .otherAttsDigest((String) entry.getProperty(OTHER_ATTRS_DIGEST)) .size((Long) entry.getProperty(FILE_SIZE)) .transferSyntaxUID((String) entry.getProperty(TRANSFER_SYNTAX)) .timeZone((String) entry.getProperty(TIME_ZONE)) .status(ctx.getObjectStatus() != null ? Status.valueOf(ctx.getObjectStatus()) : Status.ARCHIVED).build(); location.addInstance(inst); LOG.info("Create {}", location); em.persist(location); event.addTargetLocationPk(location.getPk()); } em.flush(); LocationDeleteContext srcLocationPksToDelete = (LocationDeleteContext) ctx .getProperty(SOURCE_LOCATION_PKS_TO_DELETE); if (srcLocationPksToDelete != null) { LOG.info("Source Locations to delete:{}", srcLocationPksToDelete.getLocationPks()); deleteLocations(srcLocationPksToDelete); } locationsCopied.fire(event); LOG.debug("onContainerEntriesStored for {} finished", ctx.getStorageSystemGroupID()); } private void updateStudyAccessTime(Instance inst, String storageSystemGroupID) { Study study = inst.getSeries().getStudy(); locationMgmt.findOrCreateStudyOnStorageGroup(study, storageSystemGroupID); } private void deleteLocations(LocationDeleteContext srcLocationToDeleteCtx) { List<Location> locations = em .createQuery( "SELECT l FROM Location l JOIN FETCH l.instances WHERE l.pk IN :locationPks", Location.class) .setParameter("locationPks", srcLocationToDeleteCtx.getLocationPks()) .getResultList(); ArrayList<Location> locationToDeletePks = new ArrayList<Location>(locations.size()); for (Location l : locations) { long instPk = srcLocationToDeleteCtx.getInstancePk(l.getPk()); Instance inst; for (Iterator<Instance> it = l.getInstances().iterator(); it.hasNext();) { inst = it.next(); if (inst.getPk() == instPk) { for (Iterator<Location> itL = inst.getLocations().iterator(); itL.hasNext();) { if (itL.next().getPk() == l.getPk()) { itL.remove(); break; } } it.remove(); } } em.merge(l); if (l.getInstances().size() == 0) { locationToDeletePks.add(l); LOG.info("Add Location {} to deletionPk list!", l); } } em.flush(); if (locationToDeletePks.isEmpty()) { LOG.info("No unreferenced Location to delete!"); } else { LOG.debug("Schedule deletion of source Locations:{}", locations); try { locationMgmt.scheduleDelete(locationToDeletePks, 100, false); } catch (Exception x) { LOG.error("Schedule deletion of source Locations failed! locations:{}", locations, x); } } } }