/*
* Copyright © 2015 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.data2.datafabric.dataset.service;
import co.cask.cdap.common.NamespaceNotFoundException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.http.AbstractBodyConsumer;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.namespace.NamespacedLocationFactory;
import co.cask.cdap.common.utils.DirUtils;
import co.cask.cdap.data2.datafabric.dataset.type.DatasetModuleConflictException;
import co.cask.cdap.data2.datafabric.dataset.type.DatasetTypeManager;
import co.cask.cdap.proto.DatasetModuleMeta;
import co.cask.cdap.proto.DatasetTypeMeta;
import co.cask.cdap.proto.Id;
import co.cask.cdap.store.NamespaceStore;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.BodyConsumer;
import co.cask.http.HandlerContext;
import co.cask.http.HttpResponder;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import com.google.inject.Inject;
import org.apache.twill.filesystem.Location;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
/**
* Handles dataset type management calls.
*/
// todo: do we want to make it authenticated? or do we treat it always as "internal" piece?
@Path(Constants.Gateway.API_VERSION_3 + "/namespaces/{namespace-id}")
public class DatasetTypeHandler extends AbstractHttpHandler {
public static final String HEADER_CLASS_NAME = "X-Class-Name";
private static final Logger LOG = LoggerFactory.getLogger(DatasetTypeHandler.class);
private final DatasetTypeManager manager;
private final CConfiguration cConf;
private final NamespacedLocationFactory namespacedLocationFactory;
private final NamespaceStore namespaceStore;
@Inject
public DatasetTypeHandler(DatasetTypeManager manager, CConfiguration conf,
NamespacedLocationFactory namespacedLocationFactory, NamespaceStore namespaceStore) {
this.manager = manager;
this.cConf = conf;
this.namespacedLocationFactory = namespacedLocationFactory;
this.namespaceStore = namespaceStore;
}
@Override
public void init(HandlerContext context) {
LOG.info("Starting DatasetTypeHandler");
}
@Override
public void destroy(HandlerContext context) {
LOG.info("Stopping DatasetTypeHandler");
}
@GET
@Path("/data/modules")
public void listModules(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
// Sorting by name for convenience
List<DatasetModuleMeta> list = Lists.newArrayList(manager.getModules(namespace));
Collections.sort(list, new Comparator<DatasetModuleMeta>() {
@Override
public int compare(DatasetModuleMeta o1, DatasetModuleMeta o2) {
return o1.getName().compareTo(o2.getName());
}
});
responder.sendJson(HttpResponseStatus.OK, list);
}
@DELETE
@Path("/data/modules")
public void deleteModules(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
if (Id.Namespace.SYSTEM.equals(namespace)) {
responder.sendString(HttpResponseStatus.FORBIDDEN,
String.format("Cannot delete modules from '%s' namespace.", namespaceId));
return;
}
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
try {
manager.deleteModules(namespace);
responder.sendStatus(HttpResponseStatus.OK);
} catch (DatasetModuleConflictException e) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
}
}
@PUT
@Path("/data/modules/{name}")
public BodyConsumer addModule(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId, @PathParam("name") final String name,
@HeaderParam(HEADER_CLASS_NAME) final String className) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
if (Id.Namespace.SYSTEM.equals(namespace)) {
responder.sendString(HttpResponseStatus.FORBIDDEN,
String.format("Cannot add module to '%s' namespace.", namespaceId));
return null;
}
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
// verify namespace directory exists
final Location namespaceHomeLocation = namespacedLocationFactory.get(namespace);
if (!namespaceHomeLocation.exists()) {
String msg = String.format("Home directory %s for namespace %s not found",
namespaceHomeLocation, namespaceId);
LOG.error(msg);
responder.sendString(HttpResponseStatus.NOT_FOUND, msg);
return null;
}
// Store uploaded content to a local temp file
String namespacesDir = cConf.get(Constants.Namespace.NAMESPACES_DIR);
File localDataDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR));
File namespaceBase = new File(localDataDir, namespacesDir);
File tempDir = new File(new File(namespaceBase, namespaceId),
cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
if (!DirUtils.mkdirs(tempDir)) {
throw new IOException("Could not create temporary directory at: " + tempDir);
}
final Id.DatasetModule datasetModuleId = Id.DatasetModule.from(namespace, name);
return new AbstractBodyConsumer(File.createTempFile("dataset-", ".jar", tempDir)) {
@Override
protected void onFinish(HttpResponder responder, File uploadedFile) throws Exception {
if (className == null) {
// We have to delay until body upload is completed due to the fact that not all client is
// requesting with "Expect: 100-continue" header and the client library we have cannot handle
// connection close, and yet be able to read response reliably.
// In longer term we should fix the client, as well as the netty-http server. However, since
// this handler will be gone in near future, it's ok to have this workaround.
responder.sendString(HttpResponseStatus.BAD_REQUEST, "Required header 'class-name' is absent.");
return;
}
LOG.info("Adding module {}, class name: {}", datasetModuleId, className);
String dataFabricDir = cConf.get(Constants.Dataset.Manager.OUTPUT_DIR);
Location archiveDir = namespaceHomeLocation.append(dataFabricDir).append(name)
.append(Constants.ARCHIVE_DIR);
String archiveName = name + ".jar";
Location archive = archiveDir.append(archiveName);
// Copy uploaded content to a temporary location
Location tmpLocation = archive.getTempFile(".tmp");
try {
conflictIfModuleExists(datasetModuleId);
Locations.mkdirsIfNotExists(archiveDir);
LOG.debug("Copy from {} to {}", uploadedFile, tmpLocation);
Files.copy(uploadedFile, Locations.newOutputSupplier(tmpLocation));
// Check if the module exists one more time to minimize the window of possible conflict
conflictIfModuleExists(datasetModuleId);
// Finally, move archive to final location
LOG.debug("Storing module {} jar at {}", datasetModuleId, archive);
if (tmpLocation.renameTo(archive) == null) {
throw new IOException(String.format("Could not move archive from location: %s, to location: %s",
tmpLocation, archive));
}
manager.addModule(datasetModuleId, className, archive);
// todo: response with DatasetModuleMeta of just added module (and log this info)
LOG.info("Added module {}", datasetModuleId);
responder.sendStatus(HttpResponseStatus.OK);
} catch (Exception e) {
// In case copy to temporary file failed, or rename failed
try {
tmpLocation.delete();
} catch (IOException ex) {
LOG.warn("Failed to cleanup temporary location {}", tmpLocation);
}
if (e instanceof DatasetModuleConflictException) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
} else {
LOG.error("Failed to add module {}", name, e);
throw e;
}
}
}
};
}
@DELETE
@Path("/data/modules/{name}")
public void deleteModule(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("name") String name) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
if (Id.Namespace.SYSTEM.equals(namespace)) {
responder.sendString(HttpResponseStatus.FORBIDDEN,
String.format("Cannot delete module '%s' from '%s' namespace.", name, namespaceId));
return;
}
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
boolean deleted;
try {
deleted = manager.deleteModule(Id.DatasetModule.from(namespace, name));
} catch (DatasetModuleConflictException e) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
return;
}
if (!deleted) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
return;
}
responder.sendStatus(HttpResponseStatus.OK);
}
@GET
@Path("/data/modules/{name}")
public void getModuleInfo(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("name") String name) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
DatasetModuleMeta moduleMeta = manager.getModule(Id.DatasetModule.from(namespace, name));
if (moduleMeta == null) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
} else {
responder.sendJson(HttpResponseStatus.OK, moduleMeta);
}
}
@GET
@Path("/data/types")
public void listTypes(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
// Sorting by name for convenience
List<DatasetTypeMeta> list = Lists.newArrayList(manager.getTypes(namespace));
Collections.sort(list, new Comparator<DatasetTypeMeta>() {
@Override
public int compare(DatasetTypeMeta o1, DatasetTypeMeta o2) {
return o1.getName().compareTo(o2.getName());
}
});
responder.sendJson(HttpResponseStatus.OK, list);
}
@GET
@Path("/data/types/{name}")
public void getTypeInfo(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("name") String name) throws Exception {
Id.Namespace namespace = Id.Namespace.from(namespaceId);
// Throws NamespaceNotFoundException if the namespace does not exist
ensureNamespaceExists(namespace);
DatasetTypeMeta typeMeta = manager.getTypeInfo(Id.DatasetType.from(namespace, name));
if (typeMeta == null) {
responder.sendStatus(HttpResponseStatus.NOT_FOUND);
} else {
responder.sendJson(HttpResponseStatus.OK, typeMeta);
}
}
/**
* Checks if the given module name already exists.
*
* @param datasetModuleId {@link Id.DatasetModule} of the module to check
* @throws DatasetModuleConflictException if the module exists
*/
private void conflictIfModuleExists(Id.DatasetModule datasetModuleId) throws DatasetModuleConflictException {
if (cConf.getBoolean(Constants.Dataset.DATASET_UNCHECKED_UPGRADE)) {
return;
}
DatasetModuleMeta existing = manager.getModule(datasetModuleId);
if (existing != null) {
String message = String.format("Cannot add module %s: module with same name already exists: %s",
datasetModuleId, existing);
throw new DatasetModuleConflictException(message);
}
}
/**
* Throws an exception if the specified namespace is not the system namespace and does not exist
*/
private void ensureNamespaceExists(Id.Namespace namespace) throws Exception {
if (!Id.Namespace.SYSTEM.equals(namespace)) {
if (namespaceStore.get(namespace) == null) {
throw new NamespaceNotFoundException(namespace);
}
}
}
}