/*
* Copyright 2008 University of Dundee. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.services.blitz.impl;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import ome.api.IUpdate;
import ome.api.RawFileStore;
import ome.model.core.OriginalFile;
import ome.model.enums.ChecksumAlgorithm;
import ome.services.blitz.util.BlitzExecutor;
import ome.services.blitz.util.BlitzOnly;
import ome.services.blitz.util.ParamsCache;
import ome.services.blitz.util.ServiceFactoryAware;
import ome.services.scripts.RepoFile;
import ome.services.scripts.ScriptRepoHelper;
import ome.services.util.Executor;
import ome.system.EventContext;
import ome.system.ServiceFactory;
import ome.tools.hibernate.QueryBuilder;
import ome.util.checksum.ChecksumProviderFactory;
import ome.util.checksum.ChecksumType;
import omero.ApiUsageException;
import omero.RInt;
import omero.RType;
import omero.ResourceError;
import omero.ServerError;
import omero.ValidationException;
import omero.api.AMD_IScript_canRunScript;
import omero.api.AMD_IScript_deleteScript;
import omero.api.AMD_IScript_editScript;
import omero.api.AMD_IScript_getParams;
import omero.api.AMD_IScript_getScriptID;
import omero.api.AMD_IScript_getScriptText;
import omero.api.AMD_IScript_getScriptWithDetails;
import omero.api.AMD_IScript_getScripts;
import omero.api.AMD_IScript_getUserScripts;
import omero.api.AMD_IScript_runScript;
import omero.api.AMD_IScript_uploadOfficialScript;
import omero.api.AMD_IScript_uploadScript;
import omero.api.AMD_IScript_validateScript;
import omero.api._IScriptOperations;
import omero.grid.InteractiveProcessorPrx;
import omero.grid.JobParams;
import omero.grid.ParamsHelper.Acquirer;
import omero.grid.ProcessPrx;
import omero.grid.ProcessorPrx;
import omero.grid.ScriptProcessPrx;
import omero.grid._InteractiveProcessorOperations;
import omero.model.Experimenter;
import omero.model.ExperimenterGroup;
import omero.model.IObject;
import omero.model.Job;
import omero.model.OriginalFileI;
import omero.model.ScriptJob;
import omero.model.ScriptJobI;
import omero.util.IceMapper;
import org.apache.commons.io.FilenameUtils;
import org.hibernate.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import Ice.Current;
/**
* implementation of the IScript service interface.
*
* @author Donald MacDonald, donald@lifesci.dundee.ac.uk
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta3
*/
public class ScriptI extends AbstractAmdServant implements _IScriptOperations,
ServiceFactoryAware, BlitzOnly {
private final static Logger log = LoggerFactory.getLogger(ScriptI.class);
protected ServiceFactoryI factory;
protected ParamsCache cache;
protected final ScriptRepoHelper scripts;
protected final ChecksumProviderFactory cpf;
public ScriptI(BlitzExecutor be, ScriptRepoHelper scripts,
ChecksumProviderFactory cpf, ParamsCache cache) {
super(null, be);
this.scripts = scripts;
this.cpf = cpf;
this.cache = cache;
}
public void setServiceFactory(ServiceFactoryI sf) throws ServerError {
this.factory = sf;
}
protected Acquirer acquirer() throws ServerError {
return (Acquirer) this.factory.getServant(
this.factory.sharedResources(null).ice_getIdentity());
}
// ~ Process Service methods
// =========================================================================
public void runScript_async(AMD_IScript_runScript __cb, final long scriptID,
final Map<String, RType> inputs, final RInt waitSecs, final Current __current)
throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<ScriptProcessPrx>(){
public ScriptProcessPrx call() throws ServerError {
ScriptJob job = new ScriptJobI();
job.linkOriginalFile(new OriginalFileI(scriptID, false));
int timeout = 5;
if (waitSecs != null) {
timeout = waitSecs.getValue();
}
InteractiveProcessorPrx ipPrx = acquirer().acquireProcessor(job, timeout, __current);
_InteractiveProcessorOperations ip =
(_InteractiveProcessorOperations) factory.getServant(ipPrx.ice_getIdentity());
ProcessPrx proc = ip.execute(omero.rtypes.rmap(inputs), __current);
ScriptProcessI process = new ScriptProcessI(factory, __current, ipPrx, ip, proc);
process.setApplicationContext(factory.context);
process.setHolder(factory.holder);
return process.getProxy();
}
});
}
public void canRunScript_async(AMD_IScript_canRunScript __cb, final long scriptID,
final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Boolean>(){
public Boolean call() throws ServerError {
if (scripts.isInRepo(scriptID)) {
return true;
}
// currently assuming that if we have the scriptID then
// it will have to be visible to the user/group and
// therefore those are the values we should ask the
// processor about.
ProcessorCallbackI callback = new ProcessorCallbackI(factory);
callback.setApplicationContext(factory.context);
callback.setHolder(factory.holder);
ProcessorPrx server = callback.activateAndWait(__current);
return server != null;
}
});
}
// ~ Script Service methods
// =========================================================================
/**
* Get the id of the official script with given path.
*
* @param __cb The script context.
* @param scriptPath
* {@link OriginalFile#getPath()} of the script to find id for.
* @param __current
* ice context.
*/
public void getScriptID_async(final AMD_IScript_getScriptID __cb,
final String scriptPath, final Current __current)
throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Long>(){
public Long call() {
Long id = scripts.findInDb(scriptPath, true);
if (id == null) {
return -1L;
} else {
return id;
}
}
});
}
/**
* Upload script to the server.
*
* @param path Path to the script.
* @param scriptText
* @param __current
* ice context.
*/
public void uploadScript_async(final AMD_IScript_uploadScript __cb,
final String path, final String scriptText, final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Long>() {
public Long call() throws Exception {
OriginalFile file = makeFile(path, scriptText, __current);
file = writeContent(file, scriptText, __current);
validateParams(__current, file);
return file.getId();
}
});
}
public void uploadOfficialScript_async(
AMD_IScript_uploadOfficialScript __cb, final String path,
final String scriptText, final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Long>() {
public Long call() throws Exception {
EventContext ec = factory.getEventContext();
if ( ! ec.isCurrentUserAdmin() ) {
throw new omero.SecurityViolation(null, null, "User is not an administrator");
}
try {
// ticket:2356 - should only overwrite non-scripts
Long scriptID = scripts.findInDb(path, true);
Long fileID = scripts.findInDb(path, false);
if (scriptID != null) {
throw new ApiUsageException(null, null,
"Path already exists: " + path + "\n" +
"Use editScript to modify existing official scripts.");
} else if (fileID != null) {
log.info("Overwriting existing non-script: " + fileID);
cache.removeParams(fileID);
}
RepoFile f = scripts.write(path, scriptText);
OriginalFile file = scripts.addOrReplace(f, fileID);
validateParams(__current, file);
return file.getId();
} catch (IOException e) {
omero.ServerError se = new omero.InternalException(null, null, "Cannot write " + path);
IceMapper.fillServerError(se, e);
throw se;
}
}
});
}
public void editScript_async(final AMD_IScript_editScript __cb,
final omero.model.OriginalFile fileObject, final String scriptText,
final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, true, new Callable<Object>() {
public Object call() throws Exception {
if (fileObject == null) {
throw new ApiUsageException(null, null, "file object cannot be null");
}
OriginalFile file = null;
if (fileObject.isLoaded()) {
IceMapper mapper = new IceMapper();
file = (OriginalFile) mapper.reverse(fileObject);
} else {
file = getOriginalFileOrNull(fileObject.getId().getValue(),
__current);
}
if (file == null) {
throw new ApiUsageException(null, null, "could not find file: "
+ fileObject.getId().getValue());
}
// Removing update event
// to prevent optimistic locking
scripts.setMimetype(file);
file = updateFile(file, __current);
OriginalFile official = scripts.load(file.getId(), true);
if (official != null) {
String fullname = official.getPath() + official.getName();
RepoFile f = scripts.write(fullname, scriptText);
file = scripts.update(f, file.getId(), __current.ctx);
} else {
file = writeContent(file, scriptText, __current);
}
cache.removeParams(file.getId());
validateParams(__current, file);
return null; // void
}
});
}
/**
* Return the script with the name to the user.
*
* @param id
* see above.
* @param __current
* ice context.
* @throws ServerError
* validation, api usage.
*/
public void getScriptWithDetails_async(
final AMD_IScript_getScriptWithDetails __cb, final long id,
final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
final OriginalFile file = getOriginalFileOrNull(id, __current);
if (file == null) {
return null;
}
Map<String, RType> scr = new HashMap<String, RType>();
scr.put(loadText(file, __current), new omero.util.IceMapper().toRType(file));
return scr;
}
});
}
private String loadText(final OriginalFile file, final Ice.Current current)
throws ServerError {
if (scripts.isInRepo(file.getId())) {
try {
return scripts.read(file.getPath() + file.getName());
} catch (IOException e) {
omero.ResourceError re = new omero.ResourceError(null, null, "Failed to load " + file);
IceMapper.fillServerError(re, e);
throw re;
}
}
final Long size = file.getSize();
if (size == null || size.longValue() > Integer.MAX_VALUE
|| size.longValue() < 0) {
throw new ValidationException(null, null,
"Script size : " + size
+ " invalid on Blitz.OMERO server.");
}
return (String) factory.executor.execute(current.ctx, factory.principal,
new Executor.SimpleWork(this, "getScriptWithDetails") {
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
RawFileStore rawFileStore = sf.createRawFileStore();
try {
rawFileStore.setFileId(file.getId());
String script = new String(rawFileStore.read(0L,
(int) file.getSize().longValue()));
return script;
} finally {
rawFileStore.close();
}
}
});
}
/**
* Return the script with the name to the user.
*
* @param id
* see above.
* @param __current
* ice context.
* @throws ServerError
* validation, api usage.
*/
public void getScriptText_async(final AMD_IScript_getScriptText __cb, final long id,
final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
final OriginalFile file = getOriginalFileOrNull(id, __current);
if (file == null) {
return null;
}
return loadText(file, __current);
}
});
}
/**
* Get the Parameters of the script.
*
* @param id
* see above.
* @param __current
* Ice context
* @throws ServerError
* validation, api usage.
*/
public void getParams_async(final AMD_IScript_getParams __cb, final long id,
final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
final OriginalFile file = getOriginalFileOrNull(id, __current);
if (file == null) {
return null;
}
return cache.getParams(id, file.getHash(), __current);
}
});
}
/**
* Get Scripts will return all the scripts by id and name available on the
* server.
*
* @param __current
* ice context,
* @throws ServerError
* validation, api usage.
*/
public void getScripts_async(final AMD_IScript_getScripts __cb,
Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
List<OriginalFile> files = scripts.loadAll(true); // FIXME
IceMapper mapper = new IceMapper();
return mapper.map(files);
}
});
}
@SuppressWarnings("unchecked")
public void getUserScripts_async(AMD_IScript_getUserScripts __cb,
final List<IObject> acceptsList, final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
final QueryBuilder qb = new QueryBuilder();
qb.select("o").from("OriginalFile", "o");
if (!parseAcceptsList(qb, acceptsList)) {
long gid = factory.sessionManager
.getEventContext(factory.principal)
.getCurrentGroupId();
qb.and("o.details.group.id = " + gid);
}
List<Long> officialIds = scripts.idsInDb();
if (officialIds != null && officialIds.size() > 0) {
qb.and("o.id not in (:ids) ");
qb.paramList("ids", officialIds);
}
List<OriginalFile> files = (List<OriginalFile>)
factory.executor.execute(__current.ctx, factory.principal,
new Executor.SimpleWork(this, "getUserScripts") {
@Transactional(readOnly = true)
public Object doWork(Session session,
ServiceFactory sf) {
return qb.query(session).list();
}
});
IceMapper mapper = new IceMapper();
return mapper.map(files);
}
});
}
@SuppressWarnings("unchecked")
public void validateScript_async(AMD_IScript_validateScript __cb, final Job j,
final List<IObject> acceptsList, final Current __current) throws ServerError {
safeRunnableCall(__current, __cb, false, new Callable<Object>() {
public Object call() throws Exception {
final boolean official =
acceptsList != null && acceptsList.size() == 0;
final QueryBuilder qb = new QueryBuilder();
qb.select("o").from("Job", "j");
qb.join("j.originalFileLinks", "links", false, false);
qb.join("links.child", "o", false, false);
parseAcceptsList(qb, acceptsList);
qb.and("j.id = :id");
qb.param("id", j.getId().getValue());
OriginalFile file = (OriginalFile) factory.executor.execute(__current.ctx, factory.principal,
new Executor.SimpleWork(this, "validateScript", j.getId().getValue(), acceptsList) {
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
List<OriginalFile> files = (List<OriginalFile>) qb.query(session).list();
if (files.size() != 1) {
throw new ome.conditions.ValidationException("Found wrong number of files: " + files);
}
Long id = files.get(0).getId();
if (official) {
return scripts.load(id, session,getSqlAction(), true);
} else {
return sf.getQueryService()
.get(OriginalFile.class, id);
}
}
});
return new IceMapper().map(file);
}
});
}
/**
* Delete the script with id from the server.
*
* @param id
* the id of the script to delete.
*
*/
public void deleteScript_async(final AMD_IScript_deleteScript cb,
final long id, final Current __current) throws ServerError {
safeRunnableCall(__current, cb, true, new Callable<Object>() {
public Object call() throws Exception {
OriginalFile file = getOriginalFileOrNull(id, __current);
if (file == null) {
throw new ApiUsageException(null, null,
"No script with id " + id + " on server.");
}
deleteOriginalFile(file, __current);
return null; // void
}
});
}
// Non-public-methods
// =========================================================================
private boolean parseAcceptsList(final QueryBuilder qb,
final List<IObject> acceptsList) {
qb.where();
scripts.buildQuery(qb);
if (acceptsList != null && acceptsList.size() > 0) {
for (IObject object : acceptsList) {
if (object instanceof Experimenter) {
qb.and("o.details.owner.id = :oid");
qb.param("oid", object.getId().getValue());
} else if (object instanceof ExperimenterGroup) {
qb.and("o.details.group.id = :gid");
qb.param("gid", object.getId().getValue());
} else {
throw new ome.conditions.ValidationException(
"Unsupported accept-type: " + object);
}
}
return true;
}
return false;
}
/**
* Make the file, this is a temporary file which will be changed when the
* script is validated.
*
* @param script
* script.
* @return OriginalFile tempfile..
* @throws ServerError
*/
private OriginalFile makeFile(final String path, final String script,
Ice.Current current) throws ServerError {
OriginalFile file = new OriginalFile();
file.setName(FilenameUtils.getName(path));
file.setPath(FilenameUtils.getFullPath(path));
file.setSize((long) script.getBytes().length);
file.setHasher(new ChecksumAlgorithm("SHA1-160"));
file.setHash(cpf.getProvider(ChecksumType.SHA1)
.putBytes(script.getBytes()).checksumAsString());
scripts.setMimetype(file);
return updateFile(file, current);
}
/**
* Update the file with new data.
*
* @param file
* new file data to be updated.
* @return updated file.
* @throws ServerError
*/
private OriginalFile updateFile(final OriginalFile file, final Ice.Current current) throws ServerError {
OriginalFile updatedFile = (OriginalFile) factory.executor.execute(
current.ctx, factory.principal, new Executor.SimpleWork(this, "updateFile") {
@Transactional(readOnly = false)
public Object doWork(Session session, ServiceFactory sf) {
IUpdate update = sf.getUpdateService();
file.getDetails().setUpdateEvent(null);
return update.saveAndReturnObject(file);
}
});
return updatedFile;
}
/**
* Write the content of the script to the script to the originalfile.
*
* @param file
* file
* @param script
* script
* @throws ServerError
*/
private OriginalFile writeContent(final OriginalFile file, final String script,
final Ice.Current current) throws ServerError {
return (OriginalFile) factory.executor.execute(current.ctx, factory.principal, new Executor.SimpleWork(
this, "writeContent") {
@Transactional(readOnly = false)
public Object doWork(Session session, ServiceFactory sf) {
final byte[] buf = script.getBytes();
final RawFileStore rawFileStore = sf.createRawFileStore();
try {
rawFileStore.setFileId(file.getId());
rawFileStore.truncate(buf.length); // ticket:2337
rawFileStore.write(buf, 0, buf.length);
return rawFileStore.save();
} finally {
rawFileStore.close();
}
}
});
}
/**
* Method to delete the original file
*
* @param file
* the original file.
*
*/
private void deleteOriginalFile(final OriginalFile file, final Ice.Current current)
throws ServerError {
if (file == null) {
return;
}
if (scripts.delete(file.getId())) {
return;
}
scripts.simpleDelete(current.ctx, factory.executor, factory.principal,
file.getId());
}
/**
* Method to get the original file of the script with id. This method will
* not throw an exception, but instead will return null.
*
* @param name
* See above.
* @return original file or null if script does not exist or more than one
* script with name exists.
*/
@SuppressWarnings("unchecked")
private OriginalFile getOriginalFileOrNull(long id, final Ice.Current current) {
try {
final QueryBuilder qb = new QueryBuilder();
qb.select("o").from("OriginalFile", "o");
qb.where();
scripts.buildQuery(qb);
qb.and("o.id = :id");
qb.param("id", id);
OriginalFile file = (OriginalFile) factory.executor.execute(
current.ctx, factory.principal, new Executor.SimpleWork(this,
"getOriginalFileOrNull", id) {
@Transactional(readOnly = true)
public Object doWork(Session session, ServiceFactory sf) {
return qb.query(session).uniqueResult();
}
});
return file;
} catch (RuntimeException re) {
return null;
}
}
private void validateParams(final Current __current,
OriginalFile file) throws ServerError, ApiUsageException {
try {
JobParams params = cache.getParams(file.getId(), file.getHash(), __current);
if (params == null) {
throw new ApiUsageException(null, null, "Script error: no params found.");
}
} catch (ResourceError re) {
// Probably a user script for which there's currently
// no processor running. This doesn't signal a bad script
// like null params do, but rather just that we'll have to
// generate the params later.
} catch (ValidationException ve) {
// Disable file - ticket:2282
file.setMimetype("text/plain");
file = updateFile(file, __current);
// ticket:2184 - No longer catching ValidationException
// so that if a processor is available that users get
// feedback as quickly as possible. If there is something
// else throwing a ValidationException in this call path
// then we will have to add a new exception subclass.
throw ve;
} catch (Exception e) {
// ticket:2044. Ignoring other exceptions as well since these
// may be caused by processor misbehavior. Note: the same
// exception may be thrown again during execution
log.warn("Unexpected exception on validateParams", e);
}
}
}