/*
* Copyright © 2014 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package co.cask.cdap.examples.fileset;
import co.cask.cdap.api.Transactional;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.data.DatasetInstantiationException;
import co.cask.cdap.api.dataset.DatasetManagementException;
import co.cask.cdap.api.dataset.DatasetProperties;
import co.cask.cdap.api.dataset.InstanceConflictException;
import co.cask.cdap.api.dataset.InstanceNotFoundException;
import co.cask.cdap.api.dataset.lib.FileSet;
import co.cask.cdap.api.service.AbstractService;
import co.cask.cdap.api.service.http.AbstractHttpServiceHandler;
import co.cask.cdap.api.service.http.HttpContentConsumer;
import co.cask.cdap.api.service.http.HttpServiceRequest;
import co.cask.cdap.api.service.http.HttpServiceResponder;
import com.google.common.io.Closeables;
import com.google.gson.Gson;
import org.apache.twill.filesystem.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import javax.annotation.Nullable;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
/**
* A Service to uploads files to, or downloads files from, the "lines" and "counts" file sets.
*/
public class FileSetService extends AbstractService {
@Override
protected void configure() {
setName("FileSetService");
setDescription("A Service to uploads files to, or downloads files from, the \"lines\" and \"counts\" file sets.");
setInstances(1);
addHandler(new FileSetHandler());
}
/**
* A handler that allows reading and writing files.
*/
public static class FileSetHandler extends AbstractHttpServiceHandler {
private static final Gson GSON = new Gson();
private static final Logger LOG = LoggerFactory.getLogger(FileSetHandler.class);
/**
* Responds with the content of the file specified by the request.
*
* @param set the name of the file set
* @param filePath the relative path within the file set
*/
@GET
@Path("{fileset}")
public void read(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") String set, @QueryParam("path") String filePath) {
FileSet fileSet;
try {
fileSet = getContext().getDataset(set);
} catch (DatasetInstantiationException e) {
LOG.warn("Error instantiating file set {}", set, e);
responder.sendError(400, String.format("Invalid file set name '%s'", set));
return;
}
Location location = fileSet.getLocation(filePath);
getContext().discardDataset(fileSet);
try {
responder.send(200, location, "application/octet-stream");
} catch (IOException e) {
responder.sendError(400, String.format("Unable to read path '%s' in file set '%s'", filePath, set));
}
}
/**
* Upload the content for a new file at the location specified by thee request.
*
* @param set the name of the file set
* @param filePath the relative path within the file set
*/
@PUT
@Path("{fileset}")
public HttpContentConsumer write(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set,
@QueryParam("path") final String filePath) {
FileSet fileSet;
try {
fileSet = getContext().getDataset(set);
} catch (DatasetInstantiationException e) {
LOG.warn("Error instantiating file set {}", set, e);
responder.sendError(400, String.format("Invalid file set name '%s'", set));
return null;
}
final Location location = fileSet.getLocation(filePath);
getContext().discardDataset(fileSet);
try {
final WritableByteChannel channel = Channels.newChannel(location.getOutputStream());
return new HttpContentConsumer() {
@Override
public void onReceived(ByteBuffer chunk, Transactional transactional) throws Exception {
channel.write(chunk);
}
@Override
public void onFinish(HttpServiceResponder responder) throws Exception {
channel.close();
responder.sendStatus(200);
}
@Override
public void onError(HttpServiceResponder responder, Throwable failureCause) {
Closeables.closeQuietly(channel);
try {
location.delete();
} catch (IOException e) {
LOG.warn("Failed to delete {}", location, e);
}
LOG.debug("Unable to write path '{}' in file set '{}'", filePath, set, failureCause);
responder.sendError(400, String.format("Unable to write path '%s' in file set '%s'. Reason: '%s'",
filePath, set, failureCause.getMessage()));
}
};
} catch (IOException e) {
responder.sendError(400, String.format("Unable to write path '%s' in file set '%s'. Reason: '%s'",
filePath, set, e.getMessage()));
return null;
}
}
/**
* Create a new file set. The properties for the new dataset can be given as JSON in the body
* of the request. Alternatively the request can specify the name of an existing dataset as a query
* parameter; in that case, a copy of the properties of that dataset is used to create the new file set.
* If neither a body nor a clone parameter is present, the dataset is created with empty (that is, default)
* properties.
*
* @param set the name of the file set
* @param clone the name of an existing dataset. If present, its properties are used for the new dataset.
*/
@POST
@Path("{fileset}/create")
public void create(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set,
@Nullable @QueryParam("clone") final String clone) throws DatasetManagementException {
DatasetProperties properties = DatasetProperties.EMPTY;
ByteBuffer content = request.getContent();
if (clone != null) {
try {
properties = getContext().getAdmin().getDatasetProperties(clone);
} catch (InstanceNotFoundException e) {
responder.sendError(404, "Dataset '" + clone + "' does not exist");
return;
}
} else if (content != null && content.hasRemaining()) {
try {
properties = GSON.fromJson(Bytes.toString(content), DatasetProperties.class);
} catch (Exception e) {
responder.sendError(400, "Invalid properties: " + e.getMessage());
return;
}
}
try {
getContext().getAdmin().createDataset(set, "fileSet", properties);
} catch (InstanceConflictException e) {
responder.sendError(409, "Dataset '" + set + "' already exists");
return;
}
responder.sendStatus(200);
}
/**
* Update the properties of a file set. The new properties must be be given as JSON in the body
* of the request. If no properties are given, the dataset is updated with empty properties.
*
* @param set the name of the file set
*/
@POST
@Path("{fileset}/update")
public void update(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set) throws DatasetManagementException {
DatasetProperties properties = DatasetProperties.EMPTY;
ByteBuffer content = request.getContent();
if (content != null && content.hasRemaining()) {
try {
properties = GSON.fromJson(Bytes.toString(content), DatasetProperties.class);
} catch (Exception e) {
responder.sendError(400, "Invalid properties: " + e.getMessage());
return;
}
}
try {
getContext().getAdmin().updateDataset(set, properties);
} catch (InstanceNotFoundException e) {
responder.sendError(404, "Dataset '" + set + "' does not exist");
return;
}
responder.sendStatus(200);
}
/**
* Drop an existing file set.
*
* @param set the name of the file set to drop
*/
@POST
@Path("{fileset}/drop")
public void drop(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set) throws DatasetManagementException {
try {
getContext().getAdmin().dropDataset(set);
} catch (InstanceNotFoundException e) {
responder.sendError(404, "Dataset '" + set + "' does not exist");
return;
}
responder.sendStatus(200);
}
/**
* Truncate an existing file set. This will delete all files under the file set's base path.
*
* @param set the name of the file set to truncate
*/
@POST
@Path("{fileset}/truncate")
public void truncate(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set) throws DatasetManagementException {
try {
getContext().getAdmin().truncateDataset(set);
} catch (InstanceNotFoundException e) {
responder.sendError(404, "Dataset '" + set + "' does not exist");
return;
}
responder.sendStatus(200);
}
/**
* Responds with the properties of an existing file set. The properties are returned in JSON format.
*
* @param set the name of the file set
*/
@POST
@Path("{fileset}/properties")
public void properties(HttpServiceRequest request, HttpServiceResponder responder,
@PathParam("fileset") final String set) throws DatasetManagementException {
try {
DatasetProperties props = getContext().getAdmin().getDatasetProperties(set);
responder.sendJson(200, props);
} catch (InstanceNotFoundException e) {
responder.sendError(404, "Dataset '" + set + "' does not exist");
}
}
}
}