/*
* 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.gateway.handlers;
import co.cask.cdap.api.annotation.Beta;
import co.cask.cdap.api.artifact.ArtifactScope;
import co.cask.cdap.api.data.schema.Schema;
import co.cask.cdap.api.plugin.PluginClass;
import co.cask.cdap.app.program.ManifestFields;
import co.cask.cdap.common.ArtifactAlreadyExistsException;
import co.cask.cdap.common.ArtifactNotFoundException;
import co.cask.cdap.common.ArtifactRangeNotFoundException;
import co.cask.cdap.common.BadRequestException;
import co.cask.cdap.common.NamespaceNotFoundException;
import co.cask.cdap.common.NotFoundException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.conf.PluginClassDeserializer;
import co.cask.cdap.common.http.AbstractBodyConsumer;
import co.cask.cdap.common.namespace.NamespaceAdmin;
import co.cask.cdap.internal.app.runtime.artifact.ArtifactDescriptor;
import co.cask.cdap.internal.app.runtime.artifact.ArtifactDetail;
import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository;
import co.cask.cdap.internal.app.runtime.artifact.WriteConflictException;
import co.cask.cdap.internal.app.runtime.plugin.PluginEndpoint;
import co.cask.cdap.internal.app.runtime.plugin.PluginNotExistsException;
import co.cask.cdap.internal.app.runtime.plugin.PluginService;
import co.cask.cdap.internal.io.SchemaTypeAdapter;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.artifact.ApplicationClassInfo;
import co.cask.cdap.proto.artifact.ApplicationClassSummary;
import co.cask.cdap.proto.artifact.ArtifactInfo;
import co.cask.cdap.proto.artifact.ArtifactRange;
import co.cask.cdap.proto.artifact.ArtifactSummary;
import co.cask.cdap.proto.artifact.InvalidArtifactRangeException;
import co.cask.cdap.proto.artifact.PluginInfo;
import co.cask.cdap.proto.artifact.PluginSummary;
import co.cask.cdap.proto.id.Ids;
import co.cask.cdap.proto.id.NamespaceId;
import co.cask.cdap.security.spi.authorization.UnauthorizedException;
import co.cask.http.AbstractHttpHandler;
import co.cask.http.BodyConsumer;
import co.cask.http.HttpResponder;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.jboss.netty.buffer.ChannelBufferInputStream;
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.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipException;
import javax.annotation.Nullable;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
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;
/**
* {@link co.cask.http.HttpHandler} for managing artifacts.
*/
@Singleton
@Path(Constants.Gateway.API_VERSION_3)
public class ArtifactHttpHandler extends AbstractHttpHandler {
private static final Logger LOG = LoggerFactory.getLogger(ArtifactHttpHandler.class);
private static final String VERSION_HEADER = "Artifact-Version";
private static final String EXTENDS_HEADER = "Artifact-Extends";
private static final String PLUGINS_HEADER = "Artifact-Plugins";
private static final Type APPCLASS_SUMMARIES_TYPE = new TypeToken<List<ApplicationClassSummary>>() { }.getType();
private static final Type APPCLASS_INFOS_TYPE = new TypeToken<List<ApplicationClassInfo>>() { }.getType();
private static final Type MAP_STRING_STRING_TYPE = new TypeToken<Map<String, String>>() { }.getType();
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(Schema.class, new SchemaTypeAdapter())
.registerTypeAdapter(PluginClass.class, new PluginClassDeserializer())
.create();
private static final Type PLUGINS_TYPE = new TypeToken<Set<PluginClass>>() { }.getType();
private final ArtifactRepository artifactRepository;
private final NamespaceAdmin namespaceAdmin;
private final File tmpDir;
private final PluginService pluginService;
@Inject
ArtifactHttpHandler(CConfiguration cConf, ArtifactRepository artifactRepository, NamespaceAdmin namespaceAdmin,
PluginService pluginService) {
this.namespaceAdmin = namespaceAdmin;
this.artifactRepository = artifactRepository;
this.tmpDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR),
cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
this.pluginService = pluginService;
}
@POST
@Path("/namespaces/system/artifacts")
public void refreshSystemArtifacts(HttpRequest request, HttpResponder responder) throws Exception {
try {
artifactRepository.addSystemArtifacts();
responder.sendStatus(HttpResponseStatus.OK);
} catch (IOException e) {
LOG.error("Error while refreshing system artifacts.", e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"There was an IO error while refreshing system artifacts, please try again.");
} catch (WriteConflictException e) {
LOG.error("Error while refreshing system artifacts.", e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts")
public void getArtifacts(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@Nullable @QueryParam("scope") String scope)
throws NamespaceNotFoundException, BadRequestException {
try {
if (scope == null) {
NamespaceId namespace = validateAndGetNamespace(namespaceId);
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getArtifacts(namespace, true));
} else {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getArtifacts(namespace, false));
}
} catch (IOException e) {
LOG.error("Exception reading artifact metadata for namespace {} from the store.", namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error reading artifact metadata from the store.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}")
public void getArtifactVersions(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
try {
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getArtifacts(namespace, artifactName));
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifacts named " + artifactName + " not found.");
} catch (IOException e) {
LOG.error("Exception reading artifacts named {} for namespace {} from the store.", artifactName, namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error reading artifact metadata from the store.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}")
public void getArtifactInfo(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
ArtifactDetail detail = artifactRepository.getArtifact(artifactId);
ArtifactDescriptor descriptor = detail.getDescriptor();
// info hides some fields that are available in detail, such as the location of the artifact
ArtifactInfo info = new ArtifactInfo(descriptor.getArtifactId(),
detail.getMeta().getClasses(), detail.getMeta().getProperties());
responder.sendJson(HttpResponseStatus.OK, info, ArtifactInfo.class, GSON);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception reading artifacts named {} for namespace {} from the store.", artifactName, namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error reading artifact metadata from the store.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties")
public void getProperties(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@QueryParam("scope") @DefaultValue("user") String scope,
@QueryParam("keys") @Nullable String keys)
throws NamespaceNotFoundException, BadRequestException {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
ArtifactDetail artifactDetail = artifactRepository.getArtifact(artifactId);
Map<String, String> properties = artifactDetail.getMeta().getProperties();
Map<String, String> result;
if (keys != null && !keys.isEmpty()) {
result = new HashMap<>();
for (String key : Splitter.on(',').trimResults().split(keys)) {
result.put(key, properties.get(key));
}
} else {
result = properties;
}
responder.sendJson(HttpResponseStatus.OK, result);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception reading artifacts named {} for namespace {} from the store.", artifactName, namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading artifact properties from the store.");
}
}
@PUT
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties")
public void writeProperties(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion) throws Exception {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
Map<String, String> properties;
try (Reader reader = new InputStreamReader(new ChannelBufferInputStream(request.getContent()), Charsets.UTF_8)) {
properties = GSON.fromJson(reader, MAP_STRING_STRING_TYPE);
} catch (JsonSyntaxException e) {
throw new BadRequestException("Json Syntax Error while parsing properties from request. " +
"Please check that the properties are a json map from string to string.", e);
} catch (IOException e) {
throw new BadRequestException("Unable to read properties from the request.", e);
}
try {
artifactRepository.writeArtifactProperties(artifactId, properties);
responder.sendStatus(HttpResponseStatus.OK);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception writing properties for artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error adding properties to artifact.");
}
}
@PUT
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties/{property}")
public void writeProperty(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("property") String key) throws Exception {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
String value = request.getContent().toString(Charsets.UTF_8);
if (value == null) {
responder.sendStatus(HttpResponseStatus.OK);
return;
}
try {
artifactRepository.writeArtifactProperty(artifactId, key, value);
responder.sendStatus(HttpResponseStatus.OK);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception writing properties for artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error writing property to artifact.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties/{property}")
public void getProperty(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("property") String key)
throws NamespaceNotFoundException, BadRequestException {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
ArtifactDetail detail = artifactRepository.getArtifact(artifactId);
responder.sendString(HttpResponseStatus.OK, detail.getMeta().getProperties().get(key));
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception reading property for artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error reading properties for artifact.");
}
}
@DELETE
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties")
public void deleteProperties(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion) throws Exception {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
artifactRepository.deleteArtifactProperties(artifactId);
responder.sendStatus(HttpResponseStatus.OK);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception deleting properties for artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error deleting properties for artifact.");
}
}
@DELETE
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/properties/{property}")
public void deleteProperty(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("property") String key) throws Exception {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
artifactRepository.deleteArtifactProperty(artifactId, key);
responder.sendStatus(HttpResponseStatus.OK);
} catch (ArtifactNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, "Artifact " + artifactId + " not found.");
} catch (IOException e) {
LOG.error("Exception updating properties for artifact {}.", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error deleting property for artifact.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/extensions")
public void getArtifactPluginTypes(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException, ArtifactNotFoundException {
NamespaceId namespace = Ids.namespace(namespaceId);
NamespaceId artifactNamespace = validateAndGetScopedNamespace(namespace, scope);
Id.Artifact artifactId = validateAndGetArtifactId(artifactNamespace, artifactName, artifactVersion);
try {
SortedMap<ArtifactDescriptor, Set<PluginClass>> plugins = artifactRepository.getPlugins(namespace, artifactId);
Set<String> pluginTypes = Sets.newHashSet();
for (Set<PluginClass> pluginClasses : plugins.values()) {
for (PluginClass pluginClass : pluginClasses) {
pluginTypes.add(pluginClass.getType());
}
}
responder.sendJson(HttpResponseStatus.OK, pluginTypes);
} catch (IOException e) {
LOG.error("Exception looking up plugins for artifact {}", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading plugins for the artifact from the store.");
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}/extensions/{plugin-type}")
public void getArtifactPlugins(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("plugin-type") String pluginType,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException, ArtifactNotFoundException {
NamespaceId namespace = Ids.namespace(namespaceId);
NamespaceId artifactNamespace = validateAndGetScopedNamespace(namespace, scope);
Id.Artifact artifactId = validateAndGetArtifactId(artifactNamespace, artifactName, artifactVersion);
try {
SortedMap<ArtifactDescriptor, Set<PluginClass>> plugins =
artifactRepository.getPlugins(namespace, artifactId, pluginType);
List<PluginSummary> pluginSummaries = Lists.newArrayList();
// flatten the map
for (Map.Entry<ArtifactDescriptor, Set<PluginClass>> pluginsEntry : plugins.entrySet()) {
ArtifactDescriptor pluginArtifact = pluginsEntry.getKey();
ArtifactSummary pluginArtifactSummary = ArtifactSummary.from(pluginArtifact.getArtifactId());
for (PluginClass pluginClass : pluginsEntry.getValue()) {
pluginSummaries.add(new PluginSummary(
pluginClass.getName(), pluginClass.getType(), pluginClass.getDescription(),
pluginClass.getClassName(), pluginArtifactSummary));
}
}
responder.sendJson(HttpResponseStatus.OK, pluginSummaries);
} catch (IOException e) {
LOG.error("Exception looking up plugins for artifact {}", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading plugins for the artifact from the store.");
}
}
@Beta
@POST
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/" +
"versions/{artifact-version}/plugintypes/{plugin-type}/plugins/{plugin-name}/methods/{plugin-method}")
public void callArtifactPluginMethod(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("plugin-name") String pluginName,
@PathParam("plugin-type") String pluginType,
@PathParam("plugin-method") String methodName,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NotFoundException, BadRequestException, IOException, ClassNotFoundException, IllegalAccessException {
String requestBody = request.getContent().toString(Charsets.UTF_8);
NamespaceId namespace = Ids.namespace(namespaceId);
NamespaceId artifactNamespace = validateAndGetScopedNamespace(namespace, scope);
Id.Artifact artifactId = validateAndGetArtifactId(artifactNamespace, artifactName, artifactVersion);
if (requestBody.isEmpty()) {
throw new BadRequestException("Request body is used as plugin method parameter, " +
"Received empty request body.");
}
try {
PluginEndpoint pluginEndpoint =
pluginService.getPluginEndpoint(namespace, artifactId, pluginType, pluginName, methodName);
Object response = pluginEndpoint.invoke(GSON.fromJson(requestBody, pluginEndpoint.getMethodParameterType()));
responder.sendString(HttpResponseStatus.OK, GSON.toJson(response));
} catch (JsonSyntaxException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST,
"Unable to deserialize request body to method parameter type");
} catch (InvocationTargetException e) {
if (e.getCause() instanceof javax.ws.rs.NotFoundException) {
throw new NotFoundException(e.getCause());
} else if (e.getCause() instanceof javax.ws.rs.BadRequestException) {
throw new BadRequestException(e.getCause());
} else {
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error while invoking plugin method");
}
}
}
@GET
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/" +
"versions/{artifact-version}/extensions/{plugin-type}/plugins/{plugin-name}")
public void getArtifactPlugin(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion,
@PathParam("plugin-type") String pluginType,
@PathParam("plugin-name") String pluginName,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException, ArtifactNotFoundException {
NamespaceId namespace = Ids.namespace(namespaceId);
NamespaceId artifactNamespace = validateAndGetScopedNamespace(namespace, scope);
Id.Artifact artifactId = validateAndGetArtifactId(artifactNamespace, artifactName, artifactVersion);
try {
SortedMap<ArtifactDescriptor, PluginClass> plugins =
artifactRepository.getPlugins(namespace, artifactId, pluginType, pluginName);
List<PluginInfo> pluginInfos = Lists.newArrayList();
// flatten the map
for (Map.Entry<ArtifactDescriptor, PluginClass> pluginsEntry : plugins.entrySet()) {
ArtifactDescriptor pluginArtifact = pluginsEntry.getKey();
ArtifactSummary pluginArtifactSummary = ArtifactSummary.from(pluginArtifact.getArtifactId());
PluginClass pluginClass = pluginsEntry.getValue();
pluginInfos.add(new PluginInfo(
pluginClass.getName(), pluginClass.getType(), pluginClass.getDescription(),
pluginClass.getClassName(), pluginArtifactSummary, pluginClass.getProperties(), pluginClass.getEndpoints()));
}
responder.sendJson(HttpResponseStatus.OK, pluginInfos);
} catch (PluginNotExistsException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage());
} catch (IOException e) {
LOG.error("Exception looking up plugins for artifact {}", artifactId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading plugins for the artifact from the store.");
}
}
@GET
@Path("/namespaces/{namespace-id}/classes/apps")
public void getApplicationClasses(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@Nullable @QueryParam("scope") String scope)
throws NamespaceNotFoundException, BadRequestException {
try {
if (scope == null) {
NamespaceId namespace = validateAndGetNamespace(namespaceId);
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getApplicationClasses(namespace, true),
APPCLASS_SUMMARIES_TYPE, GSON);
} else {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getApplicationClasses(namespace, false),
APPCLASS_SUMMARIES_TYPE, GSON);
}
} catch (IOException e) {
LOG.error("Error getting app classes for namespace {}.", namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading app class information from store, please try again.");
}
}
@GET
@Path("/namespaces/{namespace-id}/classes/apps/{classname}")
public void getApplicationClasses(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("classname") String className,
@QueryParam("scope") @DefaultValue("user") String scope)
throws NamespaceNotFoundException, BadRequestException {
NamespaceId namespace = validateAndGetScopedNamespace(Ids.namespace(namespaceId), scope);
try {
responder.sendJson(HttpResponseStatus.OK, artifactRepository.getApplicationClasses(namespace, className),
APPCLASS_INFOS_TYPE, GSON);
} catch (IOException e) {
LOG.error("Error getting app classes for namespace {}.", namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error reading app class information from store, please try again.");
}
}
@POST
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}")
public BodyConsumer addArtifact(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") final String namespaceId,
@PathParam("artifact-name") final String artifactName,
@HeaderParam(VERSION_HEADER) final String artifactVersion,
@HeaderParam(EXTENDS_HEADER) final String parentArtifactsStr,
@HeaderParam(PLUGINS_HEADER) String pluginClasses)
throws NamespaceNotFoundException, BadRequestException {
final NamespaceId namespace = validateAndGetNamespace(namespaceId);
// if version is explicitly given, validate the id now. otherwise version will be derived from the manifest
// and validated there
if (artifactVersion != null && !artifactVersion.isEmpty()) {
validateAndGetArtifactId(namespace, artifactName, artifactVersion);
}
final Set<ArtifactRange> parentArtifacts = parseExtendsHeader(namespace, parentArtifactsStr);
final Set<PluginClass> additionalPluginClasses;
if (pluginClasses == null) {
additionalPluginClasses = ImmutableSet.of();
} else {
try {
additionalPluginClasses = GSON.fromJson(pluginClasses, PLUGINS_TYPE);
} catch (JsonParseException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, String.format(
"%s header '%s' is invalid: %s", PLUGINS_HEADER, pluginClasses, e.getMessage()));
return null;
}
}
try {
// copy the artifact contents to local tmp directory
final File destination = File.createTempFile("artifact-", ".jar", tmpDir);
return new AbstractBodyConsumer(destination) {
@Override
protected void onFinish(HttpResponder responder, File uploadedFile) {
try {
String version = (artifactVersion == null || artifactVersion.isEmpty()) ?
getBundleVersion(uploadedFile) : artifactVersion;
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, version);
// add the artifact to the repo
artifactRepository.addArtifact(artifactId, uploadedFile, parentArtifacts, additionalPluginClasses);
responder.sendString(HttpResponseStatus.OK, "Artifact added successfully");
} catch (ArtifactRangeNotFoundException e) {
responder.sendString(HttpResponseStatus.NOT_FOUND, e.getMessage());
} catch (ArtifactAlreadyExistsException e) {
responder.sendString(HttpResponseStatus.CONFLICT, e.getMessage());
} catch (WriteConflictException e) {
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Conflict while writing artifact, please try again.");
} catch (IOException e) {
LOG.error("Exception while trying to write artifact {}-{}-{}.",
namespaceId, artifactName, artifactVersion, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error performing IO while writing artifact.");
} catch (BadRequestException e) {
responder.sendString(HttpResponseStatus.BAD_REQUEST, e.getMessage());
} catch (UnauthorizedException e) {
responder.sendString(HttpResponseStatus.FORBIDDEN, e.getMessage());
} catch (Exception e) {
LOG.error("Error while writing artifact {}-{}-{}", namespaceId, artifactName, artifactVersion, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Error while adding artifact.");
}
}
private String getBundleVersion(File file) throws BadRequestException, IOException {
try (JarFile jarFile = new JarFile(file)) {
Manifest manifest = jarFile.getManifest();
if (manifest == null) {
throw new BadRequestException(
"Unable to derive version from artifact because it does not contain a manifest. " +
"Please package the jar with a manifest, or explicitly specify the artifact version.");
}
Attributes attributes = manifest.getMainAttributes();
String version = attributes == null ? null : attributes.getValue(ManifestFields.BUNDLE_VERSION);
if (version == null) {
throw new BadRequestException(
"Unable to derive version from artifact because manifest does not contain Bundle-Version attribute. " +
"Please include Bundle-Version in the manifest, or explicitly specify the artifact version.");
}
return version;
} catch (ZipException e) {
throw new BadRequestException("Artifact is not in zip format. Please make sure it is a jar file.");
}
}
};
} catch (IOException e) {
LOG.error("Exception creating temp file to place artifact {} contents", artifactName, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR, "Server error creating temp file for artifact.");
return null;
}
}
@DELETE
@Path("/namespaces/{namespace-id}/artifacts/{artifact-name}/versions/{artifact-version}")
public void deleteArtifact(HttpRequest request, HttpResponder responder,
@PathParam("namespace-id") String namespaceId,
@PathParam("artifact-name") String artifactName,
@PathParam("artifact-version") String artifactVersion) throws Exception {
NamespaceId namespace = NamespaceId.SYSTEM.getNamespace().equalsIgnoreCase(namespaceId) ?
NamespaceId.SYSTEM : validateAndGetNamespace(namespaceId);
Id.Artifact artifactId = validateAndGetArtifactId(namespace, artifactName, artifactVersion);
try {
artifactRepository.deleteArtifact(artifactId);
responder.sendStatus(HttpResponseStatus.OK);
} catch (IOException e) {
LOG.error("Exception deleting artifact named {} for namespace {} from the store.", artifactName, namespaceId, e);
responder.sendString(HttpResponseStatus.INTERNAL_SERVER_ERROR,
"Error deleting artifact metadata from the store: " + e.getMessage());
}
}
private ArtifactScope validateScope(String scope) throws BadRequestException {
try {
return ArtifactScope.valueOf(scope.toUpperCase());
} catch (IllegalArgumentException e) {
throw new BadRequestException("Invalid scope " + scope);
}
}
private NamespaceId validateAndGetNamespace(String namespaceId) throws NamespaceNotFoundException {
return validateAndGetScopedNamespace(Ids.namespace(namespaceId), ArtifactScope.USER);
}
private NamespaceId validateAndGetScopedNamespace(NamespaceId namespace, String scope)
throws NamespaceNotFoundException, BadRequestException {
if (scope != null) {
return validateAndGetScopedNamespace(namespace, validateScope(scope));
}
return validateAndGetScopedNamespace(namespace, ArtifactScope.USER);
}
// check that the namespace exists, and check if the request is only supposed to include system artifacts,
// and returning the system namespace if so.
private NamespaceId validateAndGetScopedNamespace(NamespaceId namespace, ArtifactScope scope)
throws NamespaceNotFoundException {
try {
namespaceAdmin.get(namespace.toId());
} catch (NamespaceNotFoundException e) {
throw e;
} catch (Exception e) {
// This can only happen when NamespaceAdmin uses HTTP to interact with namespaces.
// Within AppFabric, NamespaceAdmin is bound to DefaultNamespaceAdmin which directly interacts with MDS.
// Hence, this should never happen.
throw Throwables.propagate(e);
}
return ArtifactScope.SYSTEM.equals(scope) ? NamespaceId.SYSTEM : namespace;
}
private Id.Artifact validateAndGetArtifactId(NamespaceId namespace, String name,
String version) throws BadRequestException {
try {
return Id.Artifact.from(namespace.toId(), name, version);
} catch (Exception e) {
throw new BadRequestException(e.getMessage());
}
}
// find out if this artifact extends other artifacts. If so, there will be a header like
// 'Artifact-Extends: <name>[<lowerversion>,<upperversion>]/<name>[<lowerversion>,<upperversion>]:
// for example: 'Artifact-Extends: etl-batch[1.0.0,2.0.0]/etl-realtime[1.0.0:3.0.0]
private Set<ArtifactRange> parseExtendsHeader(NamespaceId namespace, String extendsHeader)
throws BadRequestException {
Set<ArtifactRange> parentArtifacts = Sets.newHashSet();
if (extendsHeader != null) {
for (String parent : Splitter.on('/').split(extendsHeader)) {
parent = parent.trim();
ArtifactRange range;
// try parsing it as a namespaced range like system:etl-batch[1.0.0,2.0.0)
try {
range = ArtifactRange.parse(parent);
// only support extending an artifact that is in the same namespace, or system namespace
if (!range.getNamespace().equals(Id.Namespace.SYSTEM) &&
!range.getNamespace().equals(namespace.toId())) {
throw new BadRequestException(
String.format("Parent artifact %s must be in the same namespace or a system artifact.", parent));
}
} catch (InvalidArtifactRangeException e) {
// if this failed, try parsing as a non-namespaced range like etl-batch[1.0.0,2.0.0)
try {
range = ArtifactRange.parse(namespace.toId(), parent);
} catch (InvalidArtifactRangeException e1) {
throw new BadRequestException(String.format("Invalid artifact range %s: %s", parent, e1.getMessage()));
}
}
parentArtifacts.add(range);
}
}
return parentArtifacts;
}
}