/*
* Copyright (C) 2006-2015 University of Dundee & Open Microscopy Environment.
* All rights reserved.
*
* 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 2 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package ome.services;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.NonWritableChannelException;
import java.sql.SQLException;
import ome.annotations.RolesAllowed;
import ome.api.IAdmin;
import ome.api.IRepositoryInfo;
import ome.api.RawFileStore;
import ome.api.ServiceInterface;
import ome.conditions.ApiUsageException;
import ome.conditions.InternalException;
import ome.conditions.ResourceError;
import ome.conditions.RootException;
import ome.conditions.SecurityViolation;
import ome.io.nio.FileBuffer;
import ome.io.nio.OriginalFilesService;
import ome.model.core.OriginalFile;
import ome.security.policy.BinaryAccessPolicy;
import ome.util.ShallowCopy;
import ome.util.checksum.ChecksumProviderFactory;
import ome.util.checksum.ChecksumType;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.ImmutableMap;
/**
* Raw file gateway which provides access to the OMERO file repository.
*
* @author Chris Allan <a
* href="mailto:callan@blackcat.ca">callan@blackcat.ca</a>
* @version 3.0 <small> (<b>Internal version:</b> $Revision$ $Date:
* 2005/06/08 15:21:59 $) </small>
* @since OMERO3.0
*/
@Transactional(readOnly = true)
public class RawFileBean extends AbstractStatefulBean implements RawFileStore {
/**
*
*/
private static final long serialVersionUID = -450924529925301925L;
/** The logger for this particular class */
private static Logger log = LoggerFactory.getLogger(RawPixelsBean.class);
// TODO: Have the code generator give us enumeration values in ome.model,
// then ChecksumAlgorithmMapper functionality can be made widely enough
// accessible to eliminate many other copies of the values in the code-base.
/* Map of checksum algorithm names to the corresponding checksum type. */
private static final ImmutableMap<String, ChecksumType> checksumAlgorithms =
ImmutableMap.<String, ChecksumType> builder().
put("Adler-32", ChecksumType.ADLER32).
put("CRC-32", ChecksumType.CRC32).
put("MD5-128", ChecksumType.MD5).
put("Murmur3-32", ChecksumType.MURMUR32).
put("Murmur3-128", ChecksumType.MURMUR128).
put("SHA1-160", ChecksumType.SHA1).
put("File-Size-64", ChecksumType.FILE_SIZE).
build();
/** The id of the original files instance. */
private Long id;
/** Only set after a passivated bean is activated. */
private transient Long reset = null;
/** The original file this service is currently working on. */
private transient OriginalFile file;
/** The file buffer for the service's original file. */
private transient FileBuffer buffer;
/** ROMIO I/O service for files. */
private transient OriginalFilesService ioService;
/** the disk space checking service */
private transient IRepositoryInfo iRepositoryInfo;
/** admin for checking permissions of writes */
private transient IAdmin admin;
/** the checksum provider factory singleton **/
private transient ChecksumProviderFactory checksumProviderFactory;
/** is file service checking for disk overflow */
private transient boolean diskSpaceChecking;
/**
* default constructor
*/
public RawFileBean() {}
/**
* overridden to allow Spring to set boolean
* @param checking
*/
public RawFileBean(boolean checking) {
this.diskSpaceChecking = checking;
}
public Class<? extends ServiceInterface> getServiceInterface() {
return RawFileStore.class;
}
/**
* I/O service (OriginalFilesService) Bean injector.
*
* @param ioService
* an <code>OriginalFileService</code>.
*/
public final void setOriginalFilesService(OriginalFilesService ioService) {
getBeanHelper().throwIfAlreadySet(this.ioService, ioService);
this.ioService = ioService;
}
/**
* Disk Space Usage service Bean injector
* @param iRepositoryInfo
* an <code>IRepositoryInfo</code>
*/
public final void setIRepositoryInfo(IRepositoryInfo iRepositoryInfo) {
getBeanHelper().throwIfAlreadySet(this.iRepositoryInfo, iRepositoryInfo);
this.iRepositoryInfo = iRepositoryInfo;
}
public final void setAdminService(IAdmin admin) {
getBeanHelper().throwIfAlreadySet(this.admin, admin);
this.admin = admin;
}
/**
* ChecksumProviderFactory Bean injector
* @param checksumProviderFactory a <code>ChecksumProviderFactory</code>
*/
public final void setChecksumProviderFactory(
ChecksumProviderFactory checksumProviderFactory) {
getBeanHelper().throwIfAlreadySet(this.checksumProviderFactory,
checksumProviderFactory);
this.checksumProviderFactory = checksumProviderFactory;
}
// See documentation on JobBean#passivate
@RolesAllowed("user")
@Transactional(readOnly = true)
public void passivate() {
// Nothing necessary
}
// See documentation on JobBean#activate
@RolesAllowed("user")
@Transactional(readOnly = true)
public void activate() {
if (id != null) {
reset = id;
id = null;
}
}
/**
* Extends the check of the {@link #modified} flag performed by super
* with an additional check of the actual file size against the value
* stored in the database
*/
@Override
protected boolean isModified() {
if (super.isModified()) {
return true;
}
// check that the real file size doesn't differ from the DB.
// If there's no file, though, we can't lookup anyway.
if (file == null || buffer == null || file.getSize() == null) {
return false;
}
long dbSize = file.getSize();
long fileSize = size();
return dbSize != fileSize;
}
@RolesAllowed("user")
@Transactional(readOnly = false)
public synchronized OriginalFile save() {
final Long id = (file == null) ? null : file.getId();
if (id == null) {
return null;
}
if (isModified() || buffer != null && size() == 0) {
final String path = buffer.getPath();
try {
buffer.flush(true);
} catch (IOException ie) {
final String msg = "cannot flush " + path + ": " + ie;
log.warn(msg);
clean();
throw new ResourceError(msg);
}
try {
if (file.getHasher() != null) {
final ChecksumType checksumType = checksumAlgorithms.get(file.getHasher().getValue());
file.setHash(this.checksumProviderFactory
.getProvider(checksumType).putFile(path).checksumAsString());
}
File f = new File(path);
long size = f.length();
file.setSize(size);
file.setMtime(new java.sql.Timestamp(f.lastModified()));
} catch (RuntimeException re) {
// ticket:3140
if (re.getCause() instanceof FileNotFoundException) {
String msg = "Cannot find path. Deleted? " + path;
log.warn(msg);
clean(); // Prevent a second exception on close.
throw new ResourceError(msg);
}
throw re;
}
iUpdate.flush();
modified = false;
return new ShallowCopy().copy(file);
}
return null;
}
/*
* (non-Javadoc)
*
* @see ome.api.StatefulServiceInterface#close()
*/
@RolesAllowed("user")
@Transactional(readOnly = false)
public synchronized void close() {
try {
save();
} catch (RootException root) {
// ticket:3140
// if one of our exceptions, then just rethrow
throw root;
} catch (RuntimeException re) {
Long id = (file == null ? null : file.getId());
log.error("Failed to update file: " + id, re);
} finally {
clean();
}
}
public void clean() {
ioService = null;
file = null;
closeFileBuffer();
buffer = null;
}
/**
* Close the active file buffer, cleaning up any potential messes left by
* the file buffer itself.
*/
private void closeFileBuffer()
{
try
{
if (buffer != null)
buffer.close();
}
catch (IOException e)
{
if (log.isDebugEnabled()) {
log.debug("Buffer could not be closed successfully.", e);
}
throw new ResourceError(
e.getMessage() + " Please check server log.");
}
}
/*
* (non-Javadoc)
*
* @see ome.api.RawFileStore#getFileId()
*/
@RolesAllowed("user")
@Transactional(readOnly = true)
public synchronized Long getFileId() {
return id;
}
/*
* (non-Javadoc)
*
* @see ome.api.RawFileStore#setFileId(long)
*/
@RolesAllowed("user")
@Transactional(readOnly = true)
public synchronized void setFileId(final long fileId) {
setFileIdWithBuffer(fileId, null);
}
public synchronized void setFileIdWithBuffer(final long fileId,
final FileBuffer buffer) {
if (id == null || id.longValue() != fileId) {
id = new Long(fileId);
file = null;
closeFileBuffer();
this.buffer = null;
modified = false;
file = iQuery.get(OriginalFile.class, fileId);
String mode = "r";
try {
if (admin.canUpdate(file)) {
mode = "rw";
}
} catch (InternalException ie) {
// ticket:10657 this is caused by the current
// group being set to "-1" meaning no write permission
// logic can be assumed.
log.warn("No permissions info: using 'r' as mode for file " + fileId);
}
if (buffer == null) {
// If no buffer has been provided, then we check that this is
// omero.data.dir (i.e. no repository) since otherwise our
// use of ioService will not function.
String repo = (String) iQuery.execute(new HibernateCallback<String>(){
public String doInHibernate(Session arg0)
throws HibernateException, SQLException {
return (String) arg0.createSQLQuery(
"select repo from originalfile where id = ?")
.setParameter(0, fileId)
.uniqueResult();
}});
if (repo != null) {
throw new RuntimeException(repo);
}
this.buffer = ioService.getFileBuffer(file, mode);
} else {
this.buffer = buffer;
}
}
}
private synchronized void errorIfNotLoaded() {
// If we're not loaded because of passivation, then load.
if (reset != null) {
id = null;
setFileId(reset.longValue());
reset = null;
}
if (buffer == null) {
throw new ApiUsageException(
"This RawFileStore has not been properly initialized.\n"
+ "Please set the file id before executing any other methods.\n");
}
}
/* (non-Javadoc)
* @see ome.api.RawFileStore#exists()
*/
@RolesAllowed("user")
public boolean exists() {
errorIfNotLoaded();
return new File(buffer.getPath()).exists();
}
/*
* {@inheritDoc}
* @see ome.io.nio.FileBuffer#read(java.nio.ByteBuffer, long)
*/
@RolesAllowed("user")
public byte[] read(long position, int length) {
errorIfNotLoaded();
sec.checkRestriction(BinaryAccessPolicy.NAME, file);
byte[] rawBuf = new byte[length];
ByteBuffer buf = ByteBuffer.wrap(rawBuf);
try {
buffer.read(buf, position);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Buffer could not be read.", e);
}
throw new ResourceError(e.getMessage());
}
return rawBuf;
}
@RolesAllowed("user")
public boolean truncate(long length) {
errorIfNotLoaded();
try {
if (length < buffer.size()) {
buffer.truncate(length);
modified();
return true;
}
return false;
} catch (NonWritableChannelException nwce) {
throw new SecurityViolation("File not writeable!");
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Buffer write did not occur.", e);
}
throw new ResourceError(e.getMessage());
}
}
@RolesAllowed("user")
public long size() {
errorIfNotLoaded();
try {
return buffer.size();
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Buffer write did not occur.", e);
}
throw new ResourceError(e.getMessage());
}
}
@RolesAllowed("user")
public void write(byte[] buf, long position, int length) {
errorIfNotLoaded();
ByteBuffer nioBuffer = MappedByteBuffer.wrap(buf);
nioBuffer.limit(length);
if (diskSpaceChecking) {
iRepositoryInfo.sanityCheckRepository();
}
try {
do {
position += buffer.write(nioBuffer, position);
} while (nioBuffer.hasRemaining());
// Write was successful, update state.
modified();
} catch (NonWritableChannelException nwce) {
throw new SecurityViolation("File not writeable!");
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Buffer write did not occur.", e);
}
throw new ResourceError(e.getMessage());
}
}
/**
* getter disk overflow checking
* @return See above.
*/
public boolean isDiskSpaceChecking() {
return diskSpaceChecking;
}
/**
* setter disk overflow checking
* @param diskSpaceChecking
* a <code>boolean</code>
*/
public void setDiskSpaceChecking(boolean diskSpaceChecking) {
this.diskSpaceChecking = diskSpaceChecking;
}
}