/*
* $Id$
*
* Copyright 2009 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.blitz.repo;
import java.io.File;
import java.nio.channels.OverlappingFileLockException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hibernate.Session;
import org.springframework.context.ApplicationListener;
import org.springframework.transaction.annotation.Transactional;
import Ice.Current;
import Ice.ObjectAdapter;
import ome.services.blitz.fire.Registry;
import ome.services.messages.DeleteLogMessage;
import ome.services.messages.DeleteLogsMessage;
import ome.services.util.Executor;
import ome.system.Principal;
import ome.system.ServiceFactory;
import ome.util.SqlAction;
import ome.util.SqlAction.DeleteLog;
import ome.util.messages.InternalMessage;
import omero.ServerError;
import omero.api.RawFileStorePrx;
import omero.api.RawPixelsStorePrx;
import omero.api.RenderingEnginePrx;
import omero.api.ThumbnailStorePrx;
import omero.cmd.Response;
import omero.constants.SESSIONUUID;
import omero.grid.InternalRepositoryPrx;
import omero.grid.RawAccessRequest;
import omero.grid.RepositoryPrx;
import omero.grid.RepositoryPrxHelper;
import omero.grid._InternalRepositoryDisp;
import omero.model.OriginalFile;
import omero.model.OriginalFileI;
import omero.util.IceMapper;
/**
* Base repository class responsible for properly handling directory
* {@link #takeover() takeover} and other lifecycle tasks. Individual instances
* will be responsible for providing the other service instances which are
* returned from this service.
*
* @since Beta4.2
*/
public abstract class AbstractRepositoryI extends _InternalRepositoryDisp
implements ApplicationListener<InternalMessage> {
private final static Logger log = LoggerFactory.getLogger(AbstractRepositoryI.class);
private final Ice.ObjectAdapter oa;
private final Registry reg;
private final Executor ex;
private final Principal p;
private final FileMaker fileMaker;
private final PublicRepositoryI servant;
private OriginalFile description;
private RepositoryPrx proxy;
private String repoUuid;
private volatile AtomicReference<State> state = new AtomicReference<State>();
private enum State {
ACTIVE, EAGER, WAITING, CLOSED;
}
public AbstractRepositoryI(Ice.ObjectAdapter oa, Registry reg, Executor ex,
Principal p, String repoDir, PublicRepositoryI servant) {
this(oa, reg, ex, p, new FileMaker(repoDir), servant);
}
public AbstractRepositoryI(Ice.ObjectAdapter oa, Registry reg, Executor ex,
Principal p, FileMaker fileMaker, PublicRepositoryI servant) {
this.state.set(State.EAGER);
this.p = p;
this.oa = oa;
this.ex = ex;
this.reg = reg;
this.fileMaker = fileMaker;
this.servant = servant;
log.info("Initializing repository in " + fileMaker.getDir());
}
/**
* Called when this repository is creating a new {@link OriginalFile}
* repository object.
*/
public String generateRepoUuid() {
return UUID.randomUUID().toString();
}
public void onApplicationEvent(InternalMessage im) {
if (im instanceof DeleteLogMessage) {
handleDLMs(Arrays.asList((DeleteLogMessage) im));
} else if (im instanceof DeleteLogsMessage) {
handleDLMs(((DeleteLogsMessage) im).getMessages());
}
}
private void handleDLMs(List<DeleteLogMessage> dlms) {
final Ice.Current rootCurrent = new Ice.Current();
rootCurrent.ctx = new HashMap<String, String>();
rootCurrent.ctx.put(SESSIONUUID.value, p.toString());
final RepositoryDao dao = servant.repositoryDao;
final List<DeleteLog> templates = new ArrayList<DeleteLog>();
for (DeleteLogMessage dlm : dlms) {
final DeleteLog template = new DeleteLog();
template.repo = repoUuid; // Ourselves!
template.fileId = dlm.getFileId();
templates.add(template);
}
// Length matches that of dlms
final List<List<DeleteLog>> logs = dao.findRepoDeleteLogs(templates,
rootCurrent);
final Map<DeleteLog, Integer> successes =
new HashMap<DeleteLog, Integer>();
for (int i = 0; i < dlms.size(); i++) {
final DeleteLogMessage dlm = dlms.get(i);
final List<DeleteLog> dls = logs.get(i);
for (DeleteLog dl : dls) {
// Copied from RawAccessRequestI.local
String filename = dl.path + "/" + dl.name;
if (filename.startsWith("/")) {
filename = "." + filename;
}
try {
final CheckedPath checked = servant.checkPath(filename, null, null /* i.e. as admin*/);
if (!checked.delete()) {
Throwable t = new omero.grid.FileDeleteException(
null, null, "Delete file failed: " + filename);
dlm.error(dl, t);
}
} catch (Throwable t) {
log.warn("Failed to delete log " + dl, t);
dlm.error(dl, t);
}
if (!dlm.isError(dl)) {
successes.put(dl, i);
}
}
}
// Only remove the logs if req.local was successful
List<DeleteLog> copies = new ArrayList<DeleteLog>(successes.keySet());
List<Integer> counts = dao.deleteRepoDeleteLogs(copies, rootCurrent);
for (int i = 0; i < copies.size(); i++) {
DeleteLog copy = copies.get(i);
Integer index = successes.get(copy);
DeleteLogMessage dlm = dlms.get(index);
int expected = logs.get(index).size();
int actual = counts.get(i);
if (actual != expected) {
log.warn(String.format(
"Failed to remove all delete log entries: %s instead of %s",
actual, expected));
}
dlm.success(copy);
}
}
/**
* Method called in a background thread which may end up waiting
* indefinitely on the repository lock file
* ("${omero.data.dir}/.omero/repository/${omero.db.uuid}/repo_uuid").
*/
public boolean takeover() {
if (!state.compareAndSet(State.EAGER, State.WAITING)) {
log.debug("Skipping takeover");
return false;
}
// All code paths after this point should guarantee that they set
// the state to the proper code, since now no other thread can get
// into this method.
Object rv = null;
try {
GetOrCreateRepo gorc = new GetOrCreateRepo(this);
rv = ex.execute(p, gorc);
if (rv instanceof ome.model.core.OriginalFile) {
ome.model.core.OriginalFile r = (ome.model.core.OriginalFile) rv;
description = getDescription(r.getId());
proxy = gorc.publicPrx;
// Success
if (!state.compareAndSet(State.WAITING, State.ACTIVE)) {
// But this may have been set to CLOSED
log.debug("Could not set state to ACTIVE");
}
return true;
} else if (rv instanceof Exception) {
log.error("Failed during repository takeover", (Exception) rv);
} else {
log.error("Unknown issue with repository takeover:" + rv);
}
} catch (Exception e) {
log.error("Unexpected error in called executor on takeover", e);
}
state.compareAndSet(State.WAITING, State.EAGER);
return false;
}
public void close() {
state.set(State.CLOSED);
log.info("Releasing " + fileMaker.getDir());
fileMaker.close();
}
public final String getRepoUuid() {
return repoUuid;
}
public final Ice.Communicator getCommunicator() {
return oa.getCommunicator();
}
public final ObjectAdapter getObjectAdapter() {
return oa;
}
public final OriginalFile getDescription(Current __current) {
return description;
}
public final RepositoryPrx getProxy(Current __current) {
return proxy;
}
public Response rawAccess(RawAccessRequest req, Current __current) throws ServerError {
if (!(req instanceof RawAccessRequestI)) {
return new omero.cmd.ERR();
}
try {
((RawAccessRequestI) req).local(this, servant, __current);
return new omero.cmd.OK();
} catch (Throwable t) {
throw new IceMapper().handleServerError(t, servant.context);
}
}
public abstract String getFilePath(final OriginalFile file,
Current __current) throws ServerError;
// UNIMPLEMENTED
// =========================================================================
public RawFileStorePrx createRawFileStore(OriginalFile file,
Current __current) {
return null;
}
public RawPixelsStorePrx createRawPixelsStore(OriginalFile file,
Current __current) {
// TODO Auto-generated method stub
return null;
}
public RenderingEnginePrx createRenderingEngine(OriginalFile file,
Current __current) {
// TODO Auto-generated method stub
return null;
}
public ThumbnailStorePrx createThumbnailStore(OriginalFile file,
Current __current) {
// TODO Auto-generated method stub
return null;
}
// Helpers
// =========================================================================
/**
* Action class for either looking up the repository for this instance, or
* if it doesn't exist, creating it. This is the bulk of the logic for the
* {@link AbstractRepositoryI#takeover()} method, but doesn't deal with the
* atomic locking of {@link AbstractRepositoryI#state} nor error handling.
* Instead it simple returns an {@link Exception} ("failure") or null
* ("success").
*/
class GetOrCreateRepo extends Executor.SimpleWork {
private final AbstractRepositoryI repo;
RepositoryPrx publicPrx;
public GetOrCreateRepo(AbstractRepositoryI repo) {
super(repo, "takeover");
this.repo = repo;
}
@Transactional(readOnly = false)
public Object doWork(Session session, ServiceFactory sf) {
ome.model.core.OriginalFile r = null;
try {
if (fileMaker.needsInit()) {
fileMaker.init(sf.getConfigService().getDatabaseUuid());
}
String line = null;
try {
line = fileMaker.getLine();
} catch (OverlappingFileLockException ofle) {
InternalRepositoryPrx[] repos = reg.lookupRepositories();
InternalRepositoryPrx prx = null;
if (repos != null) {
for (int i = 0; i < repos.length; i++) {
if (repos[i] != null) {
if (repos[i].toString().contains(repoUuid)) {
prx = repos[i];
}
}
}
}
if (prx == null) {
fileMaker.close();
FileMaker newFileMaker = new FileMaker(new File(
fileMaker.getDir()).getAbsolutePath());
fileMaker.init(sf.getConfigService().getDatabaseUuid());
line = newFileMaker.getLine();
}
}
if (line == null) {
repoUuid = repo.generateRepoUuid();
} else {
repoUuid = line;
}
r = sf.getQueryService()
.findByString(ome.model.core.OriginalFile.class,
"hash", repoUuid);
final String path = FilenameUtils.normalize(
new File(fileMaker.getDir()).getAbsolutePath());
final String pathName = FilenameUtils.getName(path);
final String pathDir = FilenameUtils.getFullPath(path);
if (r == null) {
if (line != null) {
log.warn("Couldn't find repository object: " + line);
}
r = new ome.model.core.OriginalFile();
r.setHash(repoUuid);
r.setName(pathName);
r.setPath(pathDir);
Timestamp t = new Timestamp(System.currentTimeMillis());
r.setAtime(t);
r.setMtime(t);
r.setCtime(t);
r.setMimetype("Repository"); // ticket:2211
r.setSize(0L);
r = sf.getUpdateService().saveAndReturnObject(r);
// ticket:1794
sf.getAdminService().moveToCommonSpace(r);
fileMaker.writeLine(repoUuid);
log.info(String.format(
"Registered new repository %s (uuid=%s)", r
.getName(), repoUuid));
} else if (!r.getPath().equals(pathDir) ||
!r.getName().equals(pathName)) {
final String oldPath = r.getPath();
final String oldName = r.getName();
r.setPath(pathDir);
r.setName(pathName);
r = sf.getUpdateService().saveAndReturnObject(r);
log.warn("Data directory moved: {}{} updated to {}{}",
oldPath, oldName, pathDir, pathName);
}
// ticket:1794 - only adds if necessary
sf.getAdminService().moveToCommonSpace(r);
log.info(String.format("Opened repository %s (uuid=%s)", r
.getName(), repoUuid));
//
// Servants
//
servant.initialize(fileMaker, r.getId(), repoUuid);
LinkedList<Ice.ObjectPrx> objs = new LinkedList<Ice.ObjectPrx>();
objs.add(addOrReplace("InternalRepository-", repo));
objs.add(addOrReplace("PublicRepository-", servant.tie()));
publicPrx = RepositoryPrxHelper.uncheckedCast(objs.getLast());
//
// Activation & Registration
//
oa.activate(); // Must happen before the registry tries to connect
for (Ice.ObjectPrx prx : objs) {
reg.addObject(prx);
}
log.info("Repository now active");
return r;
} catch (Exception e) {
fileMaker.close(); // If anything goes awry, we release for
// others!
return e;
}
}
private Ice.ObjectPrx addOrReplace(String prefix, Ice.Object obj) {
Ice.Identity id = Ice.Util.stringToIdentity(prefix + repoUuid);
Object old = oa.find(id);
if (old != null) {
oa.remove(id);
log.warn(String.format("Found %s; removing: %s", id, old));
}
oa.add(obj, id);
return oa.createDirectProxy(id);
}
}
protected OriginalFileI getDescription(final long id) throws ServerError {
ome.model.core.OriginalFile file = (ome.model.core.OriginalFile) ex
.execute(p,
new Executor.SimpleWork(this, "getDescription", id) {
@Transactional(readOnly = true)
public Object doWork(Session session,
ServiceFactory sf) {
return sf.getQueryService().findByQuery(
"select o from OriginalFile o "
+ "where o.id = " + id, null);
}
});
OriginalFileI rv = (OriginalFileI) new IceMapper().map(file);
return rv;
}
@SuppressWarnings("unchecked")
protected String getFileRepo(final OriginalFile file) throws ServerError {
if (file == null || file.getId() == null) {
throw new omero.ValidationException(null, null, "Unmanaged file");
}
Map<String, Object> map = (Map<String, Object>) ex
.executeSql(new Executor.SimpleSqlWork(this,
"getFileRepo") {
@Transactional(readOnly = true)
public Object doWork(SqlAction sql) {
return sql.repoFile(file.getId().getValue());
}
});
if (map.size() == 0) {
throw new omero.ValidationException(null, null, "Unknown file: "
+ file.getId().getValue());
}
return (String) map.get("repo");
}
}