/* ***** 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) 2013
* 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.locationmgmt.impl;
import java.io.IOException;
import java.io.Serializable;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.inject.Inject;
import javax.jms.*;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import com.mysema.query.Tuple;
import com.mysema.query.jpa.impl.JPAQuery;
import org.dcm4che3.net.Device;
import org.dcm4chee.archive.dto.ActiveService;
import org.dcm4chee.archive.entity.*;
import org.dcm4chee.archive.locationmgmt.LocationMgmt;
import org.dcm4chee.archive.processing.ActiveProcessingService;
import org.dcm4chee.storage.ObjectNotFoundException;
import org.dcm4chee.storage.StorageContext;
import org.dcm4chee.storage.conf.StorageDeviceExtension;
import org.dcm4chee.storage.conf.StorageSystem;
import org.dcm4chee.storage.conf.StorageSystemGroup;
import org.dcm4chee.storage.conf.StorageSystemStatus;
import org.dcm4chee.storage.service.StorageService;
import org.dcm4chee.storage.spi.StorageSystemProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.jms.*;
import javax.jms.Queue;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* @author Hesham Elbadawi <bsdreko@gmail.com>
*
*/
@Stateless
public class LocationMgmtEJB implements LocationMgmt {
private static final Logger LOG = LoggerFactory
.getLogger(LocationMgmtEJB.class);
private static ConcurrentHashMap<String, SyncLatch> syncLatch = new ConcurrentHashMap<String, SyncLatch>();
@Inject
private Device device;
@Inject
private StorageService storageService;
@Inject
private ActiveProcessingService activeProcessingService;
@Inject
private javax.enterprise.inject.Instance<StorageSystemProvider> storageSystemProviders;
@Resource(mappedName = "java:/JmsXA")
private ConnectionFactory connFactory;
@Resource(mappedName = "java:/queue/delete")
private Queue deleteQueue;
@PersistenceContext(name = "dcm4chee-arc", unitName = "dcm4chee-arc")
private EntityManager em;
@Override
public void scheduleDelete(Collection<Location> refs, int delay,
boolean checkStudyMarked) throws JMSException {
List<Long> refPks = new ArrayList<Long>(refs.size());
for (Location ref : refs) {
refPks.add(ref.getPk());
}
scheduleDeleteByPks(refPks, delay, checkStudyMarked);
}
@Override
public void scheduleDeleteByPks(Collection<Long> refPks, int delay, boolean checkStudyMarked)
throws JMSException {
if (refPks!=null && refPks.size()>0) {
try {
Connection conn = connFactory.createConnection();
try {
Session session = conn.createSession(false,
Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(deleteQueue);
Message msg = session.createMessage();
StringBuilder list = new StringBuilder();
for (Long pk : refPks) {
if (list.length()>0) list.append(",");
list.append(pk);
}
msg.setStringProperty("PKS",list.toString());
if (delay > 0)
msg.setLongProperty("_HQ_SCHED_DELIVERY",
System.currentTimeMillis() + delay);
msg.setBooleanProperty("checkStudyMarked", checkStudyMarked);
producer.send(msg);
} finally {
conn.close();
}
} catch (JMSException e) {
throw e;
}
}
}
@Override
public void failDelete(Location ref) {
ref.setStatus(Location.Status.DELETE_FAILED);
LOG.warn(
"Failed to delete file {}, setting file reference status to {}",
ref.getStoragePath(), ref.getStatus());
}
@Override
public boolean doDelete(Location ref) {
if (!ref.getInstances().isEmpty()) {
LOG.warn(
"Deletion failed! Location {} is still referenced by instances:{}",
ref, ref.getInstances());
return false;
}
StorageDeviceExtension ext = null;
try {
ext = device
.getDeviceExtensionNotNull(StorageDeviceExtension.class);
StorageSystem storageSystem = ext.getStorageSystem(
ref.getStorageSystemGroupID(), ref.getStorageSystemID());
StorageContext cxt = storageService
.createStorageContext(storageSystem);
storageService.deleteObject(cxt, ref.getStoragePath());
}
catch(ObjectNotFoundException e1) {
LOG.error("Couldn't find the file to delete {} on file system"
+ " will delete location safely - reason {}", ref, e1);
}
catch (IOException e) {
LOG.error("Error deleting location {} - reason {}", ref, e);
return false;
}
if(removeDeadFileRef(ref)) {
try {
unflagDirtySystemsCleaned(ext.getStorageSystemGroup(ref
.getStorageSystemGroupID()));
} catch (IOException e) {
LOG.debug("Failed to restore systems emergently flagged on group {} - reason {}"
, ref.getStorageSystemGroupID(), e);
}
return true;
}
else {
return false;
}
}
@SuppressWarnings("unchecked")
@Override
public int doDelete(Collection<Long> refPks, boolean checkStudyMarked) {
LOG.debug("Called doDelete refPks:{}", refPks);
if (refPks == null || refPks.isEmpty())
return 0;
int count = 0;
Query query = em.createQuery(
"SELECT l FROM Location l WHERE l.pk IN :pks", Location.class);
query.setParameter("pks", refPks);
List<Location> result = query.getResultList();
Map<String, Location> containerLocations = new HashMap<String, Location>();
Location ref;
count += result.size();
if(checkStudyMarked)
LOG.info("Attempting delete of locations marked by deleter service, locations to delete {}", result);
for (int i = 0, len = result.size(); i < len; i++) {
ref = result.get(i);
if (ref.getEntryName() == null) {
LOG.debug("Location is not in a container, we can delete stored object and entity!");
if (!doDelete(ref)) {
LOG.warn(
"Deletion of {} failed! Mark Location as DELETION_FAILED",
ref);
failDelete(ref);
count--;
}
} else {
LOG.debug("Location is in a container, we can not delete the container at this moment!");
String key = ref.getStorageSystemID() + "_&_"
+ ref.getStoragePath();
if (containerLocations.containsKey(key)) {
removeDeadFileRef(ref);
} else {
containerLocations.put(key, ref);
count--;
}
}
}
if (containerLocations.isEmpty()) {
count += deleteContainer(containerLocations);
}
return count;
}
@Override
public Location getLocation(Long pk) {
Query query = em.createQuery("SELECT l from Location l left join fetch l.instances where l.pk = ?1");
query.setParameter(1, pk);
Location l = (Location) query.getSingleResult();
return em.merge(l);
}
@Override
public void findOrCreateStudyOnStorageGroup(String studyUID, String groupID) {
Study study = findStudy(studyUID);
if(study != null)
findOrCreateStudyOnStorageGroup(study, groupID);
}
@Override
public void findOrCreateStudyOnStorageGroup(Study study, String groupID) {
String studyUID = study.getStudyInstanceUID();
StudyOnStorageSystemGroup studyOnStgSysGrp = null;
try {
studyOnStgSysGrp = findStudyOnStorageGroup(studyUID, groupID);
studyOnStgSysGrp.setAccessTime(new Date(System.currentTimeMillis()));
studyOnStgSysGrp.setMarkedForDeletion(false);
LOG.debug("##### StudyOnStorageGroup updated! study:{} groupID:{}", studyOnStgSysGrp.getStudy(), studyOnStgSysGrp.getStorageSystemGroupID());
} catch (NoResultException e) {
String key = study.getStudyInstanceUID()+"@"+groupID;
LOG.debug("##### Create StudyOnStorageGroup entry! {}, study:{}", key, study);
if (syncLatch.putIfAbsent(key, new SyncLatch(1)) == null) {
LOG.debug("##### Creating new entry {} study:{}", key, study);
try {
studyOnStgSysGrp = new StudyOnStorageSystemGroup();
studyOnStgSysGrp.setStudy(study);
studyOnStgSysGrp.setStorageSystemGroupID(groupID);
studyOnStgSysGrp.setMarkedForDeletion(false);
studyOnStgSysGrp.setAccessTime(new Date(System.currentTimeMillis()));
em.persist(studyOnStgSysGrp);
LOG.debug("##### New StudyOnStorageGroup entry persisted! {} study:{}", key, studyOnStgSysGrp.getStudy());
} catch (Exception x) {
LOG.warn("##### Creating new entry {} study:{} failed! Try find.", key, study);
try {
studyOnStgSysGrp = findStudyOnStorageGroup(studyUID, groupID);
} catch (NoResultException nre) {
LOG.error("##### StudyOnStorageGroup still not found!");
}
} finally {
SyncLatch latch = syncLatch.remove(key);
latch.studyOnStgSysGrp = studyOnStgSysGrp;
LOG.debug("##### StudyOnStorageGroup entry {} created. {} threads are waiting.", studyOnStgSysGrp, latch.countAwaiting);
latch.countDown();
}
} else {
LOG.info("##### Another thread is creating the StudyOnStorageGroup entry! Let us wait. {}", key);
SyncLatch latch = syncLatch.get(key);
if (latch != null) {
try {
latch.await();
} catch (InterruptedException x) {
LOG.info("##### Waiting thread interrupted!");
}
studyOnStgSysGrp = latch.studyOnStgSysGrp;
LOG.info("##### StudyOnStorageGroup created:{}", studyOnStgSysGrp);
}
}
}
}
@Override
public List<Instance> findInstancesDueDelete(int studyRetention,
String studyRetentionUnit, String groupID, String studyInstanceUID, String seriesInstanceUID) {
Timestamp studyDueDate = new Timestamp(getStudyDueDate(studyRetention, studyRetentionUnit).getTimeInMillis());
JPAQuery query = new JPAQuery(em);
query.from(QInstance.instance).from(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup)
.leftJoin(QInstance.instance.locations).fetch()
.leftJoin(QInstance.instance.externalRetrieveLocations)
.join(QInstance.instance.series)
.join(QInstance.instance.series.study);
query.where(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup.markedForDeletion.isFalse())
.where(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup.accessTime.before(studyDueDate))
.where(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup.storageSystemGroupID.eq(groupID))
.where(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup.study.studyInstanceUID
.eq(QInstance.instance.series.study.studyInstanceUID))
.where(QInstance.instance.rejectionNoteCode.isNull());
if(studyInstanceUID != null)
query.where(QInstance.instance.series.study.studyInstanceUID.eq(studyInstanceUID));
if(seriesInstanceUID != null)
query.where(QInstance.instance.series.seriesInstanceUID.eq(seriesInstanceUID));
query.orderBy(QStudyOnStorageSystemGroup.studyOnStorageSystemGroup.accessTime.asc());
List<Tuple> tuples = query.list(QInstance.instance, QStudyOnStorageSystemGroup.studyOnStorageSystemGroup);
List<Instance> locationsToDelete = new ArrayList<>();
for(Tuple tuple: tuples) {
tuple.get(QInstance.instance).getExternalRetrieveLocations().size();
if(!locationsToDelete.contains(tuple.get(QInstance.instance)))
locationsToDelete.add(tuple.get(QInstance.instance));
}
return locationsToDelete;
}
@SuppressWarnings("unchecked")
@Override
public List<Location> findFailedToDeleteLocations(String groupID) {
Query query = em.createQuery("SELECT l FROM Location l WHERE "
+ "l.storageSystemGroupID = ?1 AND l.status = 1"
, Location.class);
query.setParameter(1, groupID);
return query.getResultList();
}
@Override
public long calculateDataVolumePerDayInBytes(String groupID,int dvdAverageOnNDays) {
Date nDaysAgo = new Date(System.currentTimeMillis() -
(dvdAverageOnNDays * 1000 * 60 * 60 * 24));
Query query = em.createNamedQuery(Location.CALCULATE_SUM_DATA_VOLUME_PER_DAY);
query.setParameter(1,groupID).setParameter(2, nDaysAgo);
Long result = (Long) query.getSingleResult();
if(result == null)
return 0L;
return result/dvdAverageOnNDays;
}
@Override
public boolean isMarkedForDelete(String studyInstanceUID, String groupID) {
Query query = em.createNamedQuery(StudyOnStorageSystemGroup
.FIND_BY_STUDY_INSTANCE_UID_AND_GRP_UID_MARKED);
query.setParameter(1, studyInstanceUID).setParameter(2, groupID);
try{
query.getSingleResult();
return true;
}
catch (NoResultException e) {
LOG.debug("Unable to find marked study - reason {}", e);
return false;
}
}
@Override
public void markForDeletion(String studyInstanceUID, String groupID) throws NoResultException{
StudyOnStorageSystemGroup studyOnStgSysGrp = findStudyOnStorageGroup(studyInstanceUID, groupID);
studyOnStgSysGrp
.setAccessTime(new Date(System.currentTimeMillis()));
studyOnStgSysGrp.setMarkedForDeletion(true);
}
private StudyOnStorageSystemGroup findStudyOnStorageGroup(
String studyInstanceUID, String groupID) throws NoResultException{
return em
.createNamedQuery(
StudyOnStorageSystemGroup.FIND_BY_STUDY_INSTANCE_UID_AND_GRP_UID,
StudyOnStorageSystemGroup.class)
.setParameter(1, studyInstanceUID).setParameter(2, groupID)
.getSingleResult();
}
private Calendar getStudyDueDate(int studyRetention, String studyRetentionUnit) {
GregorianCalendar dueDate = new GregorianCalendar();
long now = System.currentTimeMillis();
switch (TimeUnit.valueOf(studyRetentionUnit)) {
case DAYS:
dueDate.setTimeInMillis(now - (86400000 * (long)studyRetention));
break;
case HOURS:
dueDate.setTimeInMillis(now - (3600000 * (long)studyRetention));
break;
case MINUTES:
dueDate.setTimeInMillis(now - (60000 * (long)studyRetention));
break;
default:
dueDate.setTimeInMillis(now - (1000 * (long)studyRetention));
break;
}
return dueDate;
}
private boolean removeDeadFileRef(Location ref) {
try {
em.remove(ref);
return true;
} catch (Exception e) {
LOG.error("Failed to remove File Ref {} - reason {}", ref.toString(), e);
return false;
}
}
private Study findStudy(String studyUID) {
Study study = null;
try {
study = em.createNamedQuery(
Study.FIND_BY_STUDY_INSTANCE_UID_EAGER,
Study.class).setParameter(1, studyUID)
.getSingleResult();
}
catch(NoResultException e) {
LOG.error("Unable to find study {} - failure reason {}",
studyUID, e);
}
return study;
}
@SuppressWarnings("unchecked")
private int deleteContainer(Map<String, Location> containerLocations) {
List<Location> result;
int count = 0;
Query queryRemaining = em
.createQuery(
"SELECT l from Location l WHERE l.storageSystemGroupID = :storageSystemGroupID"
+ " AND l.storageSystemID = :storageID AND l.storagePath = :storagePath",
Location.class);
for (Location l : containerLocations.values()) {
queryRemaining.setParameter("storageSystemGroupID",
l.getStorageSystemGroupID());
queryRemaining.setParameter("storageID", l.getStorageSystemID());
queryRemaining.setParameter("storagePath", l.getStoragePath());
result = queryRemaining.getResultList();
LOG.debug("Remaining Locations for container: {}", result);
if (result.size() == 1 && result.get(0).getPk() == l.getPk()) {
LOG.debug(
"Only one Location (from the delete request) reference the container {}. We can delete the container",
queryRemaining.getParameters());
if (doDelete(l)) {
count++;
} else {
LOG.warn(
"Deletion of {} failed! mark Location as DELETION_FAILED",
l);
failDelete(l);
}
} else {
LOG.debug("Container is still referenced by other Locations. Deletion skipped!");
removeDeadFileRef(l);
count++;
}
}
return count;
}
@Override
public Collection<Long> filterForMarkedForDeletionStudiesOnGroup(
Collection<Long> refPks) {
Query query = em.createQuery(
"SELECT l FROM Location l WHERE l.pk IN :pks", Location.class);
query.setParameter("pks", refPks);
List<Location> refs = query.getResultList();
Collection<Long> filteredLocations = new ArrayList<Long>();
for (Iterator<Location> iterLoc = refs.iterator(); iterLoc.hasNext(); ) {
Location loc = iterLoc.next();
for (Iterator<Instance> iterInst = loc.getInstances().iterator(); iterInst.hasNext(); ) {
Instance inst = iterInst.next();
if (isMarkedForDelete(inst.getSeries().getStudy().getStudyInstanceUID(), loc.getStorageSystemGroupID())) {
//detach here so that either loc is tied to only instance
//or loc is tied to many and won't be deleted then is kept
iterInst.remove();
inst.getLocations().remove(loc);
//remove active process
activeProcessingService.deleteActiveProcessBySOPInstanceUIDandService(inst.getSopInstanceUID(), ActiveService.DELETER_SERVICE);
//unset marked for deletion to compensate for other instances to be deleted when the current delete fails
StudyOnStorageSystemGroup studyOnStgSysGrp = findStudyOnStorageGroup(inst.getSeries()
.getStudy().getStudyInstanceUID(), loc.getStorageSystemGroupID());
em.remove(studyOnStgSysGrp);
// studyOnStgSysGrp
// .setAccessTime(new Date(System.currentTimeMillis()));
// studyOnStgSysGrp.setMarkedForDeletion(false);
if (inst.getLocations().isEmpty())
em.remove(inst);
}
}
if (loc.getInstances().isEmpty())
filteredLocations.add(loc.getPk());
}
em.flush();
return filteredLocations;
}
private void unflagDirtySystemsCleaned(StorageSystemGroup group) throws IOException{
for(String systemID : group.getStorageSystems().keySet()) {
StorageSystem system = group.getStorageSystem(systemID);
StorageSystemProvider provider = system
.getStorageSystemProvider(storageSystemProviders);
if(system.getMinFreeSpace() != null && system.getMinFreeSpaceInBytes() == -1L)
system.setMinFreeSpaceInBytes(provider.getTotalSpace()
* Integer.parseInt(system.getMinFreeSpace()
.replace("%", ""))/100);
if(provider.getUsableSpace()
> system.getMinFreeSpaceInBytes() && (system.getStorageDeviceExtension().isDirty()
|| system.getStorageSystemStatus() != StorageSystemStatus.OK )) {
system.setStorageSystemStatus(StorageSystemStatus.OK);
system.getStorageDeviceExtension().setDirty(false);
LOG.info("System {} is now ready for use, emergency condition passed", system);
}
}
}
@Override
public void purgeStudiesRejectedOrDeletedOnAllGroups() {
purgeRejectedSeries();
List<Study> toPurge = findStudiesNoStorageGroup();
if(toPurge != null)
for(Study studyNoStgGrp : toPurge) {
if(!toPurge.contains(studyNoStgGrp))
toPurge.add(studyNoStgGrp);
}
try{
for(Study studyToPurge : toPurge) {
LOG.info("Purged study rejected or completely deleted {}",
studyToPurge);
em.remove(studyToPurge);
}
}
catch(Exception e) {
LOG.error("Problem occured while purging rejected or deleted "
+ "studies - reason {}", e);
}
}
private void purgeRejectedSeries() {
List<Series> seriesToPurge = findRejectedSeries();
for(Series series : seriesToPurge) {
try{
em.remove(series);
}
catch(Exception e) {
LOG.error("Unable to remove purged series {} - reason {}", series, e);
}
LOG.info("Removed rejected series {}", series);
}
}
private List<Series> findRejectedSeries() {
List<Series> result = em.createNamedQuery(
Series.FIND_REJECTED,
Series.class).getResultList();
return result == null ? new ArrayList<Series>() : result;
}
private List<Study> findStudiesNoStorageGroup() {
return em.createNamedQuery(
StudyOnStorageSystemGroup.FIND_STUDIES_NO_STG_GROUP,
Study.class)
.getResultList();
}
private class SyncLatch extends CountDownLatch {
private int countAwaiting;
private StudyOnStorageSystemGroup studyOnStgSysGrp;
public SyncLatch(int count) {
super(count);
}
@Override
public void await() throws InterruptedException {
countAwaiting++;
super.await();
}
public StudyOnStorageSystemGroup getStudyOnStgSysGrp() {
return studyOnStgSysGrp;
}
public void setStudyOnStgSysGrp(StudyOnStorageSystemGroup studyOnStgSysGrp) {
this.studyOnStgSysGrp = studyOnStgSysGrp;
}
public int getCountAwaiting() {
return countAwaiting;
}
}
}