/* ***** 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) 2012-2014 * 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.storage.service.impl; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Instance; import javax.inject.Inject; import org.dcm4che3.conf.api.DicomConfiguration; import org.dcm4che3.conf.core.api.ConfigurationException; import org.dcm4che3.net.Device; import org.dcm4che3.util.TagUtils; import org.dcm4chee.storage.ContainerEntry; import org.dcm4chee.storage.ObjectAlreadyExistsException; import org.dcm4chee.storage.StorageContext; import org.dcm4chee.storage.conf.StorageDevice; import org.dcm4chee.storage.conf.StorageDeviceExtension; import org.dcm4chee.storage.conf.StorageSystem; import org.dcm4chee.storage.conf.StorageSystemGroup; import org.dcm4chee.storage.service.StorageService; import org.dcm4chee.storage.spi.ContainerProvider; import org.dcm4chee.storage.spi.FileCacheProvider; import org.dcm4chee.storage.spi.StorageSystemProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Gunter Zeilinger<gunterze@gmail.com> * */ @ApplicationScoped public class StorageServiceImpl implements StorageService { private static final Logger LOG = LoggerFactory.getLogger(StorageServiceImpl.class); @Inject @StorageDevice private Device device; @Inject private DicomConfiguration dicomConfiguration; @Inject private Instance<StorageSystemProvider> storageSystemProviders; @Inject private Instance<ContainerProvider> containerProviders; @Inject private Instance<FileCacheProvider> fileCacheProviders; private final AtomicBoolean mergeDeviceIsRunning = new AtomicBoolean(); /* * Maintains the active storage system index for a storage group. * The index should not be stored in the DICOM config as for parallel filesystems it is updated * constantly in a round-robin fashion to simulate a RAID-0 like behavior. * DICOM configuration is the wrong storage for runtime state with a high modification rate. * Currently this does not take into account clustering -> Think about putting the storage runtime state * into an Infinispan cache */ private final ConcurrentMap<String,Integer> storageGroup2ActiveStorageSystemIndex = new ConcurrentHashMap<>(); @Override public StorageSystem selectStorageSystem(String groupID, long reserveSpace) { return selectStorageSystem(groupID, reserveSpace, true); } public StorageSystem selectStorageSystem(String groupID, long reserveSpace, boolean asyncMergeConfig) { StorageDeviceExtension ext = device.getDeviceExtension(StorageDeviceExtension.class); StorageSystemGroup group = ext.getStorageSystemGroup(groupID); if (group == null) { throw new IllegalArgumentException("No such Storage System Group - " + groupID); } final StorageSystemSelector storageSystemSelector = new StorageSystemSelector(group, storageSystemProviders, storageGroup2ActiveStorageSystemIndex); StorageSystem selectedSystem = storageSystemSelector.selectStorageSystem(reserveSpace); if(storageSystemSelector.isConfigurationChanged()) { Runnable mergeConfigRunner = new Runnable() { @Override public void run() { if (!mergeDeviceIsRunning.compareAndSet(false, true)) { LOG.info("mergeDevice already running"); return; } try { Device modifyDevice = dicomConfiguration.findDevice(device.getDeviceName()); storageSystemSelector.mergeDeviceChanges(modifyDevice); dicomConfiguration.merge(modifyDevice); } catch (ConfigurationException e) { LOG.warn("Device {} could not be merged", device.getDeviceName(), e); } finally { mergeDeviceIsRunning.set(false); } } }; if(asyncMergeConfig) { device.execute(mergeConfigRunner); } else { mergeConfigRunner.run(); } } return selectedSystem; } @Override public StorageSystemGroup selectBestStorageSystemGroup(String groupType) { StorageDeviceExtension ext = device .getDeviceExtension(StorageDeviceExtension.class); StorageSystemGroup best = null; for (StorageSystemGroup group : ext.getStorageSystemGroups().values()) { if (!groupType.equals(group.getStorageSystemGroupType())) continue; if (best == null || best.getStorageAccessTime() > group.getStorageAccessTime()) best = group; } return best; } @Override public Path getBaseDirectory(StorageSystem system) { StorageSystemProvider provider = system.getStorageSystemProvider(storageSystemProviders); return provider.getBaseDirectory(system); } @Override public StorageContext createStorageContext(StorageSystem storageSystem) { StorageContext ctx = new StorageContext(); ctx.setStorageSystemProvider( storageSystem.getStorageSystemProvider(storageSystemProviders)); ctx.setContainerProvider( storageSystem.getContainerProvider(containerProviders)); if (storageSystem.isCacheOnStore()) ctx.setFileCacheProvider( storageSystem.getFileCacheProvider(fileCacheProviders)); ctx.setStorageSystem(storageSystem); return ctx; } @Override public OutputStream openOutputStream(final StorageContext ctx, String name) throws IOException { StorageSystemProvider provider = ctx.getStorageSystemProvider(); FileCacheProvider fileCacheProvider = ctx.getFileCacheProvider(); provider.checkWriteable(); LOG.info("Storing stream to {}@{}", name, ctx.getStorageSystem()); //wrap in dout if (fileCacheProvider == null) return toDigestOutputStream(ctx, provider.openOutputStream(ctx, name)); Path cachedFile = fileCacheProvider.toPath(ctx, name); fileCacheProvider.register(ctx, name, cachedFile); Files.createDirectories(cachedFile.getParent()); try { FileCacheOutputStream fout = new FileCacheOutputStream(ctx, name, cachedFile); return toDigestOutputStream(ctx, fout); } catch (FileAlreadyExistsException e) { throw new ObjectAlreadyExistsException( ctx.getStorageSystem().getStorageSystemPath(), name, e); } } private static class FileCacheOutputStream extends FilterOutputStream { private StorageContext ctx; private String name; private Path path; public FileCacheOutputStream(StorageContext ctx, String name, Path path) throws IOException { super(Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)); this.ctx = ctx; this.name = name; this.path = path; } @Override public void close() throws IOException { super.close(); ctx.getStorageSystemProvider().storeFile(ctx, path, name); } } @Override public void copyInputStream(StorageContext ctx, InputStream in, String name) throws IOException { StorageSystemProvider provider = ctx.getStorageSystemProvider(); FileCacheProvider fileCacheProvider = ctx.getFileCacheProvider(); provider.checkWriteable(); if (fileCacheProvider != null) { Path cachedFile = fileCacheProvider.toPath(ctx, name); fileCacheProvider.register(ctx, name, cachedFile); Files.createDirectories(cachedFile.getParent()); try { calculateDigestAndCopy(ctx, in, cachedFile); } catch (FileAlreadyExistsException e) { throw new ObjectAlreadyExistsException( ctx.getStorageSystem().getStorageSystemPath(), name, e); } provider.storeFile(ctx, cachedFile, name); } else provider.copyInputStream(ctx, toDigestInputStream(ctx, in), name); LOG.info("Copied stream to {}@{}", name, ctx.getStorageSystem()); } @Override public void storeContainerEntries(StorageContext ctx, List<ContainerEntry> entries, String name) throws IOException { ContainerProvider containerProvider = ctx.getContainerProvider(); if (containerProvider == null) throw new UnsupportedOperationException(); StorageSystemProvider provider = ctx.getStorageSystemProvider(); provider.checkWriteable(); try ( OutputStream out = provider.openOutputStream(ctx, name)) { containerProvider.writeEntriesTo(ctx, entries, out); } LOG.info("Stored Entries to {}@{}", name, ctx.getStorageSystem()); FileCacheProvider fileCacheProvider = ctx.getFileCacheProvider(); if (fileCacheProvider != null) { for (ContainerEntry entry : entries) { Path cachedFile = fileCacheProvider .toPath(ctx, name).resolve(entry.getName()); Files.createDirectories(cachedFile.getParent()); Files.copy(entry.getSourcePath(), cachedFile); fileCacheProvider.register(ctx, name, cachedFile); } } } @Override public void storeFile(StorageContext ctx, Path path, String name) throws IOException { StorageSystemProvider provider = ctx.getStorageSystemProvider(); FileCacheProvider fileCacheProvider = ctx.getFileCacheProvider(); provider.checkWriteable(); provider.storeFile(ctx, path, name); if (fileCacheProvider != null) { Path cachedFile = fileCacheProvider.toPath(ctx, name); Files.createDirectories(cachedFile.getParent()); Files.copy(path, cachedFile); fileCacheProvider.register(ctx, name, cachedFile); } LOG.info("Stored File {} to {}@{}", path, name, ctx.getStorageSystem()); } @Override public void moveFile(StorageContext ctx, Path path, String name) throws IOException { StorageSystemProvider provider = ctx.getStorageSystemProvider(); FileCacheProvider fileCacheProvider = ctx.getFileCacheProvider(); provider.checkWriteable(); if (fileCacheProvider != null) { provider.storeFile(ctx, path, name); Path cachedFile = fileCacheProvider.toPath(ctx, name); Files.createDirectories(cachedFile.getParent()); Files.move(path, cachedFile); fileCacheProvider.register(ctx, name, cachedFile); } else { provider.moveFile(ctx, path, name); } LOG.info("Moved File {} to {}@{}", path, name, ctx.getStorageSystem()); } @Override public void deleteObject(StorageContext context, String name) throws IOException { StorageSystemProvider provider = context.getStorageSystemProvider(); provider.checkWriteable(); provider.deleteObject(context, name); LOG.info("Delete Object {}@{}", name, context.getStorageSystem()); } @Override public void syncFiles(StorageSystem storageSystem, List<String> names) throws IOException { StorageSystemProvider provider = storageSystem.getStorageSystemProvider(storageSystemProviders); provider.sync(names); } protected void calculateDigestAndCopy(StorageContext ctx, InputStream in, Path cachedFile) throws IOException { InputStream din = toDigestInputStream(ctx, in); if(din instanceof DigestInputStream) Files.copy(din, cachedFile); else Files.copy(din, cachedFile); } protected InputStream toDigestInputStream(final StorageContext ctx, InputStream in) { String digestAlgorithm = ctx.getStorageSystem().getStorageSystemGroup() .getDigestAlgorithm(); if (digestAlgorithm != null) { MessageDigest digest; try { digest = MessageDigest.getInstance(digestAlgorithm); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Invalid digest algorithm," + " check configuration for storage group" + ctx.getStorageSystem().getStorageSystemGroup() .getGroupID()); } DigestInputStream din = new DigestInputStream(in, digest){ @Override public void close() throws IOException { super.close(); ctx.setFileDigest(TagUtils.toHexString(getMessageDigest().digest())); } }; return din; } else { return in; } } protected OutputStream toDigestOutputStream(final StorageContext ctx, OutputStream out) { DigestOutputStream dout = null; String digestAlgorithm = ctx.getStorageSystem() .getStorageSystemGroup().getDigestAlgorithm(); if (digestAlgorithm != null) { MessageDigest digest = null; try { digest = MessageDigest.getInstance(digestAlgorithm); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Invalid digest algorithm," + " check configuration for storage group" + ctx.getStorageSystem().getStorageSystemGroup() .getGroupID()); } dout = new DigestOutputStream(out, digest) { @Override public void close() throws IOException { super.close(); ctx.setFileDigest(TagUtils.toHexString(getMessageDigest().digest())); } }; return dout; } else { return out; } } }