/*
* Copyright 2012 JBoss 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 org.artificer.server.atom.services;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.artificer.atom.ArtificerAtomUtils;
import org.artificer.atom.err.ArtificerAtomException;
import org.artificer.atom.visitors.ArtifactToFullAtomEntryVisitor;
import org.artificer.common.ArtifactContent;
import org.artificer.common.ArtifactType;
import org.artificer.common.ArtifactVerifier;
import org.artificer.common.ArtificerConfig;
import org.artificer.common.ArtificerConstants;
import org.artificer.common.MediaType;
import org.artificer.common.error.ArtificerServerException;
import org.artificer.common.error.ArtificerUserException;
import org.artificer.common.visitors.ArtifactVisitorHelper;
import org.artificer.events.EventProducer;
import org.artificer.events.EventProducerFactory;
import org.artificer.repository.PersistenceManager;
import org.artificer.repository.RepositoryProviderFactory;
import org.artificer.server.ArtifactServiceImpl;
import org.artificer.server.i18n.Messages;
import org.artificer.server.mime.MimeTypes;
import org.jboss.resteasy.plugins.providers.atom.Entry;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.resteasy.plugins.providers.multipart.MultipartConstants;
import org.jboss.resteasy.plugins.providers.multipart.MultipartRelatedInput;
import org.jboss.resteasy.util.GenericType;
import org.oasis_open.docs.s_ramp.ns.s_ramp_v1.BaseArtifactType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
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.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Set;
/**
* The JAX-RS resource that handles artifact specific tasks, including:
*
* <ul>
* <li>Add an artifact (upload)</li>
* <li>Get an artifact (full Atom {@link Entry})</li>
* <li>Get artifact content (binary content)</li>
* <li>Update artifact meta data</li>
* <li>Update artifact content</li>
* <li>Delete an artifact</li>
* </ul>
*
* @author eric.wittmann@redhat.com
*/
@Path("/s-ramp")
public class ArtifactResource extends AbstractResource {
private static Logger logger = LoggerFactory.getLogger(ArtifactResource.class);
// Sadly, date formats are not thread safe.
private static final ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(ArtificerConstants.DATE_FORMAT);
}
};
private final ArtifactServiceImpl artifactService = new ArtifactServiceImpl();
@POST
@Path("{model}/{type}")
@Consumes(MediaType.APPLICATION_ATOM_XML_ENTRY)
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry create(@Context HttpServletRequest request,
@PathParam("model") String model, @PathParam("type") String type, Entry entry)
throws ArtificerServerException {
try {
BaseArtifactType artifact = ArtificerAtomUtils.unwrapSrampArtifact(entry);
// create artifactType here, in case it doesn't match what's actually sent
ArtifactType artifactType = ArtifactType.valueOf(model, type, false);
BaseArtifactType persistedArtifact = artifactService.create(artifactType, artifact);
return wrapArtifact(persistedArtifact, request);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
}
}
/**
* S-RAMP atom POST to upload an artifact to the repository. The artifact content should be POSTed raw.
*
* @param fileName
* @param model
* @param type
* @param is
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@POST
@Path("{model}/{type}")
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry create(@Context HttpServletRequest request, @HeaderParam("Slug") String fileName,
@PathParam("model") String model, @PathParam("type") String type, InputStream is)
throws ArtificerAtomException {
try {
BaseArtifactType artifact = artifactService.upload(model, type, fileName, is);
return wrapArtifact(artifact, request);
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
} finally {
IOUtils.closeQuietly(is);
}
}
/**
* Atom POST to upload an artifact to the repository. The artifact content is assumed to be available to the server,
* so the absolute path is provided (instead of the content itself).
*
* @param model
* @param type
* @param path
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@POST
@Path("{model}/{type}")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry create(@Context HttpServletRequest request, @PathParam("model") String model,
@PathParam("type") String type, String path)
throws ArtificerAtomException {
InputStream is = null;
try {
File file = new File(path);
is = new FileInputStream(file);
BaseArtifactType artifact = artifactService.upload(model, type, file.getName(), is);
return wrapArtifact(artifact, request);
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
} finally {
IOUtils.closeQuietly(is);
}
}
/**
* S-RAMP atom POST to upload an artifact to the repository. The artifact content should be POSTed raw. This
* endpoint does *not* require the model/type to be provided. Instead, @link{ArtifactTypeDetector} is called
* to automatically identify the type.
*
* Note that this is not required by the spec! Also note that the filename slug *is* required.
*
* The endpoint is /s-ramp/autodetect.
*
* @param fileName
* @param is
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@POST
@Path("autodetect")
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry create(@Context HttpServletRequest request, @HeaderParam("Slug") String fileName,
InputStream is) throws ArtificerAtomException {
try {
if (StringUtils.isEmpty(fileName)) {
throw ArtificerUserException.filenameRequired();
}
BaseArtifactType artifact = artifactService.upload(fileName, is);
return wrapArtifact(artifact, request);
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
} finally {
IOUtils.closeQuietly(is);
}
}
/**
* Same as {@link #create(javax.servlet.http.HttpServletRequest, String, String, String)}, but using
* type autodetection.
*
* @param path
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@POST
@Path("autodetect")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry create(@Context HttpServletRequest request, String path)
throws ArtificerAtomException {
InputStream is = null;
try {
File file = new File(path);
is = new FileInputStream(file);
BaseArtifactType artifact = artifactService.upload(file.getName(), is);
return wrapArtifact(artifact, request);
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
} finally {
IOUtils.closeQuietly(is);
}
}
private Entry wrapArtifact(BaseArtifactType artifact, HttpServletRequest request) throws Exception {
// return the entry containing the s-ramp artifact
String baseUrl = ArtificerConfig.getBaseUrl(request.getRequestURL().toString());
ArtifactToFullAtomEntryVisitor visitor = new ArtifactToFullAtomEntryVisitor(baseUrl);
ArtifactVisitorHelper.visitArtifact(visitor, artifact);
return visitor.getAtomEntry();
}
/**
* Handles multi-part creates. In S-RAMP, an HTTP multi-part request can be POST'd to the endpoint, which
* allows Atom Entry formatted meta-data to be included in the same request as the artifact content.
*
* @param model
* @param type
* @param input
* @return the newly created artifact as an Atom {@link Entry}
* @throws org.artificer.atom.err.ArtificerAtomException
* @throws org.artificer.common.error.ArtificerWrongModelException
*/
@POST
@Path("{model}/{type}")
@Consumes(MultipartConstants.MULTIPART_RELATED)
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry createMultiPart(@Context HttpServletRequest request, @PathParam("model") String model,
@PathParam("type") String type, MultipartRelatedInput input) throws ArtificerServerException {
try {
String baseUrl = ArtificerConfig.getBaseUrl(request.getRequestURL().toString());
ArtifactType artifactType = ArtifactType.valueOf(model, type, false);
if (artifactType.isDerived()) {
throw ArtificerUserException.derivedArtifactCreate(artifactType.getArtifactType());
}
if (artifactType.isExtendedType()) {
artifactType = ArtifactType.ExtendedDocument(artifactType.getExtendedType());
}
List<InputPart> list = input.getParts();
// Expecting 2 parts
if (list.size() != 2) {
throw new ArtificerAtomException(Messages.i18n.format("INVALID_MULTIPART_POST", list.size()));
}
InputPart firstPart = list.get(0);
InputPart secondpart = list.get(1);
// Getting the S-RAMP Artifact
Entry atomEntry = firstPart.getBody(new GenericType<Entry>() {});
BaseArtifactType artifactMetaData = ArtificerAtomUtils.unwrapSrampArtifact(atomEntry);
ArtifactVerifier verifier = new ArtifactVerifier(artifactType);
ArtifactVisitorHelper.visitArtifact(verifier, artifactMetaData);
verifier.throwError();
String fileName = null;
if (artifactMetaData.getName() != null)
fileName = artifactMetaData.getName();
ArtifactContent content = new ArtifactContent(fileName, secondpart.getBody(new GenericType<InputStream>() {}));
String mimeType = MimeTypes.determineMimeType(fileName, content.getInputStream(), artifactType);
artifactType.setMimeType(mimeType);
// Processing the content itself first
PersistenceManager persistenceManager = RepositoryProviderFactory.persistenceManager();
// store the content
BaseArtifactType artifactRval = persistenceManager.persistArtifact(artifactMetaData, content);
Set<EventProducer> eventProducers = EventProducerFactory.getEventProducers();
for (EventProducer eventProducer : eventProducers) {
eventProducer.artifactCreated(artifactRval);
}
// Convert to a full Atom Entry and return it
return wrapArtifact(artifactRval, request);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_ARTY"), e);
throw new ArtificerAtomException(e);
}
}
/**
* Called to update the meta data for an artifact. Note that this does *not* update the content of the
* artifact, just the meta data.
*
* @param model
* @param type
* @param uuid
* @param atomEntry
* @throws org.artificer.atom.err.ArtificerAtomException
* @throws org.artificer.common.error.ArtificerWrongModelException
*/
@PUT
@Path("{model}/{type}/{uuid}")
@Consumes(MediaType.APPLICATION_ATOM_XML_ENTRY)
public void updateMetaData(@PathParam("model") String model, @PathParam("type") String type,
@PathParam("uuid") String uuid, Entry atomEntry) throws ArtificerServerException {
try {
ArtifactType artifactType = ArtifactType.valueOf(model, type, null);
if (artifactType.isExtendedType()) {
artifactType = ArtificerAtomUtils.getArtifactType(atomEntry);
}
BaseArtifactType updatedArtifact = ArtificerAtomUtils.unwrapSrampArtifact(atomEntry);
artifactService.updateMetaData(artifactType, uuid, updatedArtifact);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Throwable e) {
logError(logger, Messages.i18n.format("ERROR_UPDATING_META_DATA", uuid), e);
throw new ArtificerAtomException(e);
}
}
@POST
@Path("{model}/{type}/{uuid}/comment")
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry addComment(@Context HttpServletRequest request, @PathParam("model") String model,
@PathParam("type") String type, @PathParam("uuid") String uuid, String text)
throws ArtificerServerException {
try {
ArtifactType artifactType = ArtifactType.valueOf(model, type, false);
BaseArtifactType artifact = artifactService.addComment(artifactType, uuid, text);
// Return the entry containing the s-ramp artifact
return wrapArtifact(artifact, request);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Exception e) {
logError(logger, Messages.i18n.format("ERROR_CREATING_COMMENT", uuid), e);
throw new ArtificerAtomException(e);
}
}
/**
* Called to get the meta data for an s-ramp artifact. This will return an Atom {@link Entry} with the
* full information about the artifact.
*
* @param model
* @param type
* @param uuid
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@GET
@Path("{model}/{type}/{uuid}")
@Produces(MediaType.APPLICATION_ATOM_XML_ENTRY)
public Entry getMetaData(@Context HttpServletRequest request, @PathParam("model") String model,
@PathParam("type") String type, @PathParam("uuid") String uuid) throws ArtificerServerException {
try {
BaseArtifactType artifact = artifactService.getMetaData(model, type, uuid);
// Return the entry containing the s-ramp artifact
return wrapArtifact(artifact, request);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Throwable e) {
logError(logger, Messages.i18n.format("ERROR_GETTING_META_DATA", uuid), e);
throw new ArtificerAtomException(e);
}
}
/**
* Returns the content of an artifact in the s-ramp repository.
*
* @param model
* @param type
* @param uuid
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@GET
@Path("{model}/{type}/{uuid}/media")
public Response getContent(@PathParam("model") String model, @PathParam("type") String type,
@PathParam("uuid") String uuid) throws ArtificerServerException {
try {
ArtifactType artifactType = ArtifactType.valueOf(model, type, true);
BaseArtifactType artifact = artifactService.getMetaData(artifactType, uuid);
final InputStream inputStream = artifactService.getContent(artifactType, artifact);
Object output = new StreamingOutput() {
@Override
public void write(OutputStream output) throws IOException, WebApplicationException {
try {
IOUtils.copy(inputStream, output);
} finally {
IOUtils.closeQuietly(inputStream);
}
}
};
String lastModifiedDate = dateFormat.get().format(
artifact.getLastModifiedTimestamp().toGregorianCalendar().getTime());
return Response
.ok(output, artifactType.getMimeType())
.header("Content-Disposition", "attachment; filename=" + artifact.getName())
.header("Content-Length",
artifact.getOtherAttributes().get(ArtificerConstants.SRAMP_CONTENT_SIZE_QNAME))
.header("Last-Modified", lastModifiedDate).build();
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Throwable e) {
logError(logger, Messages.i18n.format("ERROR_GETTING_CONTENT", uuid), e);
throw new ArtificerAtomException(e);
}
}
/**
* Called to delete an s-ramp artifact from the repository.
*
* @param model
* @param type
* @param uuid
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@DELETE
@Path("{model}/{type}/{uuid}")
public void delete(@PathParam("model") String model, @PathParam("type") String type,
@PathParam("uuid") String uuid) throws ArtificerServerException {
try {
artifactService.delete(model, type, uuid);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Throwable e) {
logError(logger, Messages.i18n.format("ERROR_DELETING_ARTY", uuid), e);
throw new ArtificerAtomException(e);
}
}
/**
* Called to forcefully delete an s-ramp artifact from the repository, ignoring all constraints.
*
* @param model
* @param type
* @param uuid
* @throws org.artificer.atom.err.ArtificerAtomException
*/
@DELETE
@Path("{model}/{type}/{uuid}/force")
public void forceDelete(@PathParam("model") String model, @PathParam("type") String type,
@PathParam("uuid") String uuid) throws ArtificerServerException {
try {
artifactService.delete(model, type, uuid, true);
} catch (ArtificerServerException e) {
// Simply re-throw. Don't allow the following catch it -- ArtificerServerException is mapped to a unique
// HTTP response type.
throw e;
} catch (Throwable e) {
logError(logger, Messages.i18n.format("ERROR_DELETING_ARTY", uuid), e);
throw new ArtificerAtomException(e);
}
}
}