blob: 48969fa0082aef93cca66e98fc530062cc8cf459 [file] [log] [blame]
/*
* Copyright (C) 2010-2012 The University of Manchester
*
* See the file "LICENSE" for license terms.
*/
package org.taverna.server.master;
import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
import static javax.ws.rs.core.Response.created;
import static javax.ws.rs.core.Response.noContent;
import static javax.ws.rs.core.Response.ok;
import static javax.ws.rs.core.Response.seeOther;
import static javax.ws.rs.core.Response.status;
import static org.apache.commons.logging.LogFactory.getLog;
import static org.taverna.server.master.api.ContentTypes.APPLICATION_ZIP_TYPE;
import static org.taverna.server.master.api.ContentTypes.DIRECTORY_VARIANTS;
import static org.taverna.server.master.api.ContentTypes.INITIAL_FILE_VARIANTS;
import static org.taverna.server.master.common.Roles.SELF;
import static org.taverna.server.master.common.Roles.USER;
import static org.taverna.server.master.common.Uri.secure;
import static org.taverna.server.master.utils.RestUtils.opt;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
import javax.xml.ws.Holder;
import org.apache.commons.logging.Log;
import org.springframework.beans.factory.annotation.Required;
import org.taverna.server.master.api.DirectoryBean;
import org.taverna.server.master.exceptions.FilesystemAccessException;
import org.taverna.server.master.exceptions.NoDirectoryEntryException;
import org.taverna.server.master.exceptions.NoUpdateException;
import org.taverna.server.master.interfaces.Directory;
import org.taverna.server.master.interfaces.DirectoryEntry;
import org.taverna.server.master.interfaces.File;
import org.taverna.server.master.interfaces.TavernaRun;
import org.taverna.server.master.rest.DirectoryContents;
import org.taverna.server.master.rest.FileSegment;
import org.taverna.server.master.rest.MakeOrUpdateDirEntry;
import org.taverna.server.master.rest.MakeOrUpdateDirEntry.MakeDirectory;
import org.taverna.server.master.rest.TavernaServerDirectoryREST;
import org.taverna.server.master.utils.FilenameUtils;
import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
import org.taverna.server.master.utils.InvocationCounter.CallCounted;
/**
* RESTful access to the filesystem.
*
* @author Donal Fellows
*/
class DirectoryREST implements TavernaServerDirectoryREST, DirectoryBean {
private Log log = getLog("Taverna.Server.Webapp");
private TavernaServerSupport support;
private TavernaRun run;
private FilenameUtils fileUtils;
@Override
public void setSupport(TavernaServerSupport support) {
this.support = support;
}
@Override
@Required
public void setFileUtils(FilenameUtils fileUtils) {
this.fileUtils = fileUtils;
}
@Override
public DirectoryREST connect(TavernaRun run) {
this.run = run;
return this;
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed({ USER, SELF })
public Response destroyDirectoryEntry(List<PathSegment> path)
throws NoUpdateException, FilesystemAccessException,
NoDirectoryEntryException {
support.permitUpdate(run);
fileUtils.getDirEntry(run, path).destroy();
return noContent().build();
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed({ USER, SELF })
public DirectoryContents getDescription(UriInfo ui)
throws FilesystemAccessException {
return new DirectoryContents(ui, run.getWorkingDirectory()
.getContents());
}
@Override
@CallCounted
public Response options(List<PathSegment> path) {
return opt("PUT", "POST", "DELETE");
}
/*
* // Nasty! This can have several different responses...
*
* @Override @CallCounted private Response
* getDirectoryOrFileContents(List<PathSegment> path, UriInfo ui, Request
* req) throws FilesystemAccessException, NoDirectoryEntryException {
*
* DirectoryEntry de = fileUtils.getDirEntry(run, path);
*
* // How did the user want the result?
*
* List<Variant> variants = getVariants(de); Variant v =
* req.selectVariant(variants); if (v == null) return
* notAcceptable(variants).type(TEXT_PLAIN)
* .entity("Do not know what type of response to produce.") .build();
*
* // Produce the content to deliver up
*
* Object result; if
* (v.getMediaType().equals(APPLICATION_OCTET_STREAM_TYPE))
*
* // Only for files...
*
* result = de; else if (v.getMediaType().equals(APPLICATION_ZIP_TYPE))
*
* // Only for directories...
*
* result = ((Directory) de).getContentsAsZip(); else
*
* // Only for directories... // XML or JSON; let CXF pick what to do
*
* result = new DirectoryContents(ui, ((Directory) de).getContents());
* return ok(result).type(v.getMediaType()).build();
*
* }
*/
private boolean matchType(MediaType a, MediaType b) {
if (log.isDebugEnabled())
log.debug("comparing " + a.getType() + "/" + a.getSubtype()
+ " and " + b.getType() + "/" + b.getSubtype());
return (a.isWildcardType() || b.isWildcardType() || a.getType().equals(
b.getType()))
&& (a.isWildcardSubtype() || b.isWildcardSubtype() || a
.getSubtype().equals(b.getSubtype()));
}
/**
* What are we willing to serve up a directory or file as?
*
* @param de
* The reference to the object to serve.
* @return The variants we can serve it as.
* @throws FilesystemAccessException
* If we fail to read data necessary to detection of its media
* type.
*/
private List<Variant> getVariants(DirectoryEntry de)
throws FilesystemAccessException {
if (de instanceof Directory)
return DIRECTORY_VARIANTS;
else if (!(de instanceof File))
throw new FilesystemAccessException("not a directory or file!");
File f = (File) de;
List<Variant> variants = new ArrayList<>(INITIAL_FILE_VARIANTS);
String contentType = support.getEstimatedContentType(f);
if (!contentType.equals(APPLICATION_OCTET_STREAM)) {
String[] ct = contentType.split("/");
variants.add(0,
new Variant(new MediaType(ct[0], ct[1]), (String) null, null));
}
return variants;
}
/** How did the user want the result? */
private MediaType pickType(HttpHeaders headers, DirectoryEntry de)
throws FilesystemAccessException, NegotiationFailedException {
List<Variant> variants = getVariants(de);
// Manual content negotiation!!! Ugh!
for (MediaType mt : headers.getAcceptableMediaTypes())
for (Variant v : variants)
if (matchType(mt, v.getMediaType()))
return v.getMediaType();
throw new NegotiationFailedException(
"Do not know what type of response to produce.", variants);
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed({ USER, SELF })
public Response getDirectoryOrFileContents(List<PathSegment> path,
UriInfo ui, HttpHeaders headers) throws FilesystemAccessException,
NoDirectoryEntryException, NegotiationFailedException {
DirectoryEntry de = fileUtils.getDirEntry(run, path);
// How did the user want the result?
MediaType wanted = pickType(headers, de);
log.info("producing content of type " + wanted);
// Produce the content to deliver up
Object result;
if (de instanceof File) {
// Only for files...
result = de;
List<String> range = headers.getRequestHeader("Range");
if (range != null && range.size() == 1)
return new FileSegment((File) de, range.get(0))
.toResponse(wanted);
} else {
// Only for directories...
Directory d = (Directory) de;
if (wanted.getType().equals(APPLICATION_ZIP_TYPE.getType())
&& wanted.getSubtype().equals(
APPLICATION_ZIP_TYPE.getSubtype()))
result = d.getContentsAsZip();
else
// XML or JSON; let CXF pick what to do
result = new DirectoryContents(ui, d.getContents());
}
return ok(result).type(wanted).build();
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed({ USER, SELF })
public Response makeDirectoryOrUpdateFile(List<PathSegment> parent,
MakeOrUpdateDirEntry op, UriInfo ui) throws NoUpdateException,
FilesystemAccessException, NoDirectoryEntryException {
support.permitUpdate(run);
DirectoryEntry container = fileUtils.getDirEntry(run, parent);
if (!(container instanceof Directory))
throw new FilesystemAccessException("You may not "
+ ((op instanceof MakeDirectory) ? "make a subdirectory of"
: "place a file in") + " a file.");
if (op.name == null || op.name.length() == 0)
throw new FilesystemAccessException("missing name attribute");
Directory d = (Directory) container;
UriBuilder ub = secure(ui).path("{name}");
// Make a directory in the context directory
if (op instanceof MakeDirectory) {
Directory target = d.makeSubdirectory(support.getPrincipal(),
op.name);
return created(ub.build(target.getName())).build();
}
// Make or set the contents of a file
File f = null;
for (DirectoryEntry e : d.getContents()) {
if (e.getName().equals(op.name)) {
if (e instanceof Directory)
throw new FilesystemAccessException(
"You may not overwrite a directory with a file.");
f = (File) e;
break;
}
}
if (f == null) {
f = d.makeEmptyFile(support.getPrincipal(), op.name);
f.setContents(op.contents);
return created(ub.build(f.getName())).build();
}
f.setContents(op.contents);
return seeOther(ub.build(f.getName())).build();
}
private File getFileForWrite(List<PathSegment> filePath,
Holder<Boolean> isNew) throws FilesystemAccessException,
NoDirectoryEntryException, NoUpdateException {
support.permitUpdate(run);
if (filePath == null || filePath.size() == 0)
throw new FilesystemAccessException(
"Cannot create a file that is not in a directory.");
List<PathSegment> dirPath = new ArrayList<>(filePath);
String name = dirPath.remove(dirPath.size() - 1).getPath();
DirectoryEntry de = fileUtils.getDirEntry(run, dirPath);
if (!(de instanceof Directory)) {
throw new FilesystemAccessException(
"Cannot create a file that is not in a directory.");
}
Directory d = (Directory) de;
File f = null;
isNew.value = false;
for (DirectoryEntry e : d.getContents())
if (e.getName().equals(name)) {
if (e instanceof File) {
f = (File) e;
break;
}
throw new FilesystemAccessException(
"Cannot create a file that is not in a directory.");
}
if (f == null) {
f = d.makeEmptyFile(support.getPrincipal(), name);
isNew.value = true;
} else
f.setContents(new byte[0]);
return f;
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed({ USER, SELF })
public Response setFileContents(List<PathSegment> filePath,
InputStream contents, UriInfo ui) throws NoDirectoryEntryException,
NoUpdateException, FilesystemAccessException {
Holder<Boolean> isNew = new Holder<>(true);
support.copyStreamToFile(contents, getFileForWrite(filePath, isNew));
if (isNew.value)
return created(ui.getAbsolutePath()).build();
else
return noContent().build();
}
@Override
@CallCounted
@PerfLogged
@RolesAllowed(USER)
public Response setFileContentsFromURL(List<PathSegment> filePath,
List<URI> referenceList, UriInfo ui)
throws NoDirectoryEntryException, NoUpdateException,
FilesystemAccessException {
support.permitUpdate(run);
if (referenceList.isEmpty() || referenceList.size() > 1)
return status(422).entity("URI list must have single URI in it")
.build();
URI uri = referenceList.get(0);
try {
uri.toURL();
} catch (MalformedURLException e) {
return status(422).entity("URI list must have value URL in it")
.build();
}
Holder<Boolean> isNew = new Holder<>(true);
File f = getFileForWrite(filePath, isNew);
try {
support.copyDataToFile(uri, f);
} catch (MalformedURLException ex) {
// Should not happen; called uri.toURL() successfully above
throw new NoUpdateException("failed to parse URI", ex);
} catch (IOException ex) {
throw new FilesystemAccessException(
"failed to transfer data from URI", ex);
}
if (isNew.value)
return created(ui.getAbsolutePath()).build();
else
return noContent().build();
}
}