/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.contentrepository.impl.endpoint;
import static ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils.DEFAULT_PREVIEW_FORMAT;
import ch.entwine.weblounge.common.content.PreviewGenerator;
import ch.entwine.weblounge.common.content.Resource;
import ch.entwine.weblounge.common.content.ResourceContent;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.ResourceUtils;
import ch.entwine.weblounge.common.content.image.ImageStyle;
import ch.entwine.weblounge.common.impl.content.image.ImageStyleUtils;
import ch.entwine.weblounge.common.impl.language.LanguageUtils;
import ch.entwine.weblounge.common.impl.request.RequestUtils;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.language.UnknownLanguageException;
import ch.entwine.weblounge.common.repository.ContentRepository;
import ch.entwine.weblounge.common.repository.ContentRepositoryException;
import ch.entwine.weblounge.common.repository.ResourceSerializer;
import ch.entwine.weblounge.common.repository.ResourceSerializerService;
import ch.entwine.weblounge.common.site.Environment;
import ch.entwine.weblounge.common.site.ImageScalingMode;
import ch.entwine.weblounge.common.site.Module;
import ch.entwine.weblounge.common.site.Site;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.osgi.service.component.ComponentContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
/**
* This class implements the <code>REST</code> endpoint for resource previews.
*/
@Path("/")
public class PreviewsEndpoint extends ContentRepositoryEndpoint {
/** The endpoint documentation */
private String docs = null;
/** The list of image styles */
private final List<ImageStyle> styles = new ArrayList<ImageStyle>();
/** The request environment */
protected Environment environment = Environment.Production;
/** The resource serializer service */
private ResourceSerializerService serializerService = null;
/**
* OSGi callback on component inactivation.
*
* @param ctx
* the component context
*/
void deactivate(ComponentContext ctx) {
styles.clear();
}
/**
* Returns the resource with the given identifier and styled using the
* requested image style or a <code>404</code> if the resource or the resource
* content could not be found.
* <p>
* If the content is not available in the requested language, the original
* language version is used.
*
* @param request
* the request
* @param resourceId
* the resource identifier
* @param languageId
* the language identifier
* @param styleId
* the image style identifier
* @return the image
*/
@GET
@Path("/{resource}/locales/{language}/styles/{style}")
public Response getPreview(@Context HttpServletRequest request,
@PathParam("resource") String resourceId,
@PathParam("language") String languageId,
@PathParam("style") String styleId,
@QueryParam("version") @DefaultValue("0") long version,
@QueryParam("force") @DefaultValue("false") boolean force) {
// Check the parameters
if (resourceId == null)
throw new WebApplicationException(Status.BAD_REQUEST);
// Get the resource
final Site site = getSite(request);
final Resource<?> resource = loadResource(request, resourceId, null, version);
if (resource == null)
throw new WebApplicationException(Status.NOT_FOUND);
// Extract the language
Language language = null;
try {
language = LanguageUtils.getLanguage(languageId);
if (!resource.supportsLanguage(language)) {
if (!resource.contents().isEmpty())
language = resource.getOriginalContent().getLanguage();
else if (resource.supportsLanguage(site.getDefaultLanguage()))
language = site.getDefaultLanguage();
else if (resource.languages().size() == 1)
language = resource.languages().iterator().next();
else
throw new WebApplicationException(Status.NOT_FOUND);
}
} catch (UnknownLanguageException e) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
// Search the site for the image style
ImageStyle style = null;
for (Module m : site.getModules()) {
style = m.getImageStyle(styleId);
if (style != null) {
break;
}
}
// Search the global styles
if (style == null) {
for (ImageStyle s : styles) {
if (s.getIdentifier().equals(styleId)) {
style = s;
break;
}
}
}
// The image style was not found
if (style == null)
throw new WebApplicationException(Status.BAD_REQUEST);
// Load the input stream from the scaled image
File scaledResourceFile = ImageStyleUtils.getScaledFile(resource, language, style);
// Is there an up-to-date, cached version on the client side?
if (!ResourceUtils.hasChanged(request, scaledResourceFile)) {
return Response.notModified().build();
}
ResourceURI resourceURI = resource.getURI();
final ContentRepository contentRepository = getContentRepository(site, false);
// When there is no scaling required, just return the original
if (ImageScalingMode.None.equals(style.getScalingMode())) {
return getResourceContent(request, resource, language);
}
// Find a serializer
ResourceSerializer<?, ?> serializer = serializerService.getSerializerByType(resourceURI.getType());
if (serializer == null)
throw new WebApplicationException(Status.PRECONDITION_FAILED);
// Does the serializer come with a preview generator?
PreviewGenerator previewGenerator = serializer.getPreviewGenerator(resource);
if (previewGenerator == null)
throw new WebApplicationException(Status.NOT_FOUND);
// Load the resource contents from the repository
InputStream resourceInputStream = null;
long contentLength = -1;
// Load the input stream from the scaled image
InputStream contentRepositoryIs = null;
FileOutputStream fos = null;
try {
long resourceLastModified = ResourceUtils.getModificationDate(resource, language).getTime();
if (!scaledResourceFile.isFile() || scaledResourceFile.lastModified() < resourceLastModified) {
if (!force)
throw new WebApplicationException(Response.Status.NOT_FOUND);
contentRepositoryIs = contentRepository.getContent(resourceURI, language);
scaledResourceFile = ImageStyleUtils.createScaledFile(resource, language, style);
scaledResourceFile.setLastModified(Math.max(new Date().getTime(), resourceLastModified));
fos = new FileOutputStream(scaledResourceFile);
logger.debug("Creating scaled image '{}' at {}", resource, scaledResourceFile);
previewGenerator.createPreview(resource, environment, language, style, DEFAULT_PREVIEW_FORMAT, contentRepositoryIs, fos);
if (scaledResourceFile.length() == 0) {
logger.debug("Error scaling '{}': file size is 0", resourceURI);
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
}
}
// Did scaling work? If not, cleanup and tell the user
if (scaledResourceFile.length() == 0)
throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
// The scaled resource should now exist
resourceInputStream = new FileInputStream(scaledResourceFile);
contentLength = scaledResourceFile.length();
} catch (WebApplicationException e) {
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
if (scaledResourceFile != null)
deleteIfEmpty(scaledResourceFile.getParentFile());
throw e;
} catch (ContentRepositoryException e) {
logger.error("Error loading {} image '{}' from {}: {}", new Object[] {
language,
resource,
contentRepository,
e.getMessage() });
logger.error(e.getMessage(), e);
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
if (scaledResourceFile != null)
deleteIfEmpty(scaledResourceFile.getParentFile());
throw new WebApplicationException();
} catch (IOException e) {
logger.error("Error scaling image '{}': {}", resourceURI, e.getMessage());
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
if (scaledResourceFile != null)
deleteIfEmpty(scaledResourceFile.getParentFile());
throw new WebApplicationException();
} catch (IllegalArgumentException e) {
logger.error("Image '{}' is of unsupported format: {}", resourceURI, e.getMessage());
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
if (scaledResourceFile != null)
deleteIfEmpty(scaledResourceFile.getParentFile());
throw new WebApplicationException();
} catch (Throwable t) {
logger.error("Error scaling image '{}': {}", resourceURI, t.getMessage());
IOUtils.closeQuietly(resourceInputStream);
FileUtils.deleteQuietly(scaledResourceFile);
if (scaledResourceFile != null)
deleteIfEmpty(scaledResourceFile.getParentFile());
throw new WebApplicationException();
} finally {
IOUtils.closeQuietly(contentRepositoryIs);
IOUtils.closeQuietly(fos);
}
// Create the response
final InputStream is = resourceInputStream;
ResponseBuilder response = Response.ok(new StreamingOutput() {
public void write(OutputStream os) throws IOException,
WebApplicationException {
try {
IOUtils.copy(is, os);
os.flush();
} catch (IOException e) {
if (!RequestUtils.isCausedByClient(e))
logger.error("Error writing preview to client", e);
} finally {
IOUtils.closeQuietly(is);
}
}
});
// Add mime type header
String mimetype = previewGenerator.getContentType(resource, language, style);
if (mimetype == null)
mimetype = MediaType.APPLICATION_OCTET_STREAM;
response.type(mimetype);
// Add last modified header
response.lastModified(new Date(scaledResourceFile.lastModified()));
// Add ETag header
String eTag = ResourceUtils.getETagValue(scaledResourceFile);
response.tag(eTag);
// Add filename header
String filename = null;
ResourceContent resourceContent = resource.getContent(language);
if (resourceContent != null)
filename = resourceContent.getFilename();
if (StringUtils.isBlank(filename))
filename = scaledResourceFile.getName();
response.header("Content-Disposition", "inline; filename=" + filename);
// Content length
response.header("Content-Length", Long.toString(contentLength));
// Send the response
return response.build();
}
/**
* Deletes the preview images for the given resource and language.
*
* @param request
* the request
* @param resourceId
* the resource identifier
* @param languageId
* the language identifier
*/
@POST
@Path("/")
public Response createPreviews(@Context HttpServletRequest request) {
Site site = super.getSite(request);
final ContentRepository contentRepository = getContentRepository(site, false);
new Thread(new Runnable() {
public void run() {
try {
contentRepository.createPreviews();
} catch (ContentRepositoryException e) {
logger.warn("Preview generation returned with an error: {}", e.getMessage());
}
}
}).start();
return Response.ok().build();
}
/**
* Deletes all preview images.
*
* @param request
* the request
*/
@DELETE
@Path("/")
public Response removePreviews(@Context HttpServletRequest request) {
Site site = super.getSite(request);
File previewsDir = ImageStyleUtils.getDirectory(site);
if (FileUtils.deleteQuietly(previewsDir))
return Response.ok().build();
else
return Response.serverError().build();
}
/**
* Deletes all preview images for the given resource.
*
* @param request
* the request
* @param resourceId
* the resource identifier
*/
@DELETE
@Path("/{resource}")
public Response removePreviewsByStyle(@Context HttpServletRequest request,
@PathParam("resource") String styleId) {
return removePreview(request, null, null, null);
}
/**
* Deletes the preview images for the given style and language.
*
* @param request
* the request
* @param resourceId
* the resource identifier
* @param languageId
* the language identifier
*/
@DELETE
@Path("/styles/{style}")
public Response removePreview(@Context HttpServletRequest request,
@PathParam("style") String styleId) {
Site site = super.getSite(request);
// Check the parameters
if (styleId == null)
throw new WebApplicationException(Status.BAD_REQUEST);
// Search the site for the image style
ImageStyle style = null;
for (Module m : site.getModules()) {
style = m.getImageStyle(styleId);
if (style != null) {
break;
}
}
// Search the global styles
if (style == null) {
for (ImageStyle s : styles) {
if (s.getIdentifier().equals(styleId)) {
style = s;
break;
}
}
}
// The image style was not found
if (style == null)
throw new WebApplicationException(Status.BAD_REQUEST);
File previewsDir = ImageStyleUtils.getDirectory(site, style);
if (FileUtils.deleteQuietly(previewsDir))
return Response.ok().build();
else
return Response.serverError().build();
}
/**
* Deletes the preview images for the given resource, the language and image
* style.
*
* @param request
* the request
* @param resourceId
* the resource identifier
* @param languageId
* the language identifier
* @param styleId
* the image style identifier
*/
@DELETE
@Path("/{resource}/locales/{language}/styles/{style}")
public Response removePreview(@Context HttpServletRequest request,
@PathParam("resource") String resourceId,
@PathParam("language") String languageId,
@PathParam("style") String styleId) {
// Check the parameters
if (resourceId == null)
throw new WebApplicationException(Status.BAD_REQUEST);
// Get the resource
final Site site = getSite(request);
final Resource<?> resource = loadResource(request, resourceId, null);
if (resource == null)
throw new WebApplicationException(Status.NOT_FOUND);
// Extract the language
List<Language> languages = new ArrayList<Language>();
if (languageId != null) {
try {
languages.add(LanguageUtils.getLanguage(languageId));
} catch (UnknownLanguageException e) {
throw new WebApplicationException(Status.BAD_REQUEST);
}
} else {
languages.addAll(resource.languages());
}
// Search the site for the image style (if applicable)
List<ImageStyle> removeStyles = new ArrayList<ImageStyle>();
if (styleId != null) {
for (Module m : site.getModules()) {
ImageStyle s = m.getImageStyle(styleId);
if (s != null) {
removeStyles.add(s);
break;
}
}
// Search the global styles
if (removeStyles.size() > 0) {
for (ImageStyle s : this.styles) {
if (s.getIdentifier().equals(styleId)) {
removeStyles.add(s);
break;
}
}
}
} else {
removeStyles.addAll(this.styles);
for (Module m : site.getModules()) {
removeStyles.addAll(Arrays.asList(m.getImageStyles()));
}
}
ResourceURI resourceURI = resource.getURI();
final ContentRepository contentRepository = getContentRepository(site, false);
// Load the resource versions from the repository
ResourceURI[] versions = null;
try {
versions = contentRepository.getVersions(resourceURI);
} catch (ContentRepositoryException e1) {
throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
}
// Remove the preview for all versions in the specified styles and languages
for (ResourceURI u : versions) {
for (ImageStyle style : removeStyles) {
for (Language language : languages) {
deletePreview(resource, u.getVersion(), style, language);
}
}
}
// Send the response
return Response.status(Status.OK).build();
}
/**
* Deletes a single preview image.
*
* @param resource
* the resource
* @param version
* the resource version
* @param style
* the image style
* @param language
* the language
*/
private void deletePreview(Resource<?> resource, long version,
ImageStyle style, Language language) {
// Find a serializer
ResourceSerializer<?, ?> serializer = serializerService.getSerializerByType(resource.getURI().getType());
if (serializer == null)
throw new WebApplicationException(Status.PRECONDITION_FAILED);
// Does the serializer come with a preview generator?
PreviewGenerator previewGenerator = serializer.getPreviewGenerator(resource);
if (previewGenerator == null)
throw new WebApplicationException(Status.NOT_FOUND);
// Load the input stream from the scaled image
File scaledResourceFile = null;
try {
scaledResourceFile = ImageStyleUtils.getScaledFile(resource, language, style);
if (scaledResourceFile.exists()) {
logger.debug("Deleting preview at {}", scaledResourceFile);
FileUtils.deleteQuietly(scaledResourceFile);
File parentDir = scaledResourceFile.getParentFile();
while (parentDir.isDirectory() && parentDir.list().length == 0) {
logger.debug("Deleting empty preview directory {}", parentDir);
FileUtils.deleteQuietly(parentDir);
parentDir = parentDir.getParentFile();
}
}
} catch (Throwable t) {
logger.error("Error removing preview image '{}': {}", resource.getURI(), t.getMessage());
throw new WebApplicationException();
}
}
/**
* Deletes the directory if it is empty and tries the same for the parent
* directory.
*
* @param dir
* the directory
*/
private void deleteIfEmpty(File dir) {
while (dir != null && dir.isDirectory() && (dir.listFiles() == null || dir.listFiles().length == 0)) {
FileUtils.deleteQuietly(dir);
dir = dir.getParentFile();
}
}
/**
* Returns the list of image styles that are registered for a site.
*
* @param request
* the request
* @return the list of image styles
*/
@GET
@Produces(MediaType.TEXT_XML)
@Path("/styles")
public Response getImagestyles(@Context HttpServletRequest request) {
Site site = getSite(request);
if (site == null)
throw new WebApplicationException(Status.NOT_FOUND);
StringBuffer buf = new StringBuffer("<styles>");
// Add styles of current site
for (Module m : site.getModules()) {
ImageStyle[] styles = m.getImageStyles();
for (ImageStyle style : styles) {
buf.append(style.toXml());
}
}
// Add global styles
for (ImageStyle style : styles) {
buf.append(style.toXml());
}
buf.append("</styles>");
ResponseBuilder response = Response.ok(buf.toString());
return response.build();
}
/**
* Returns the image styles or a <code>404</code>.
*
* @param request
* the request
* @param styleId
* the image style identifier
* @return the image
*/
@GET
@Produces(MediaType.TEXT_XML)
@Path("/styles/{style}")
public Response getImagestyle(@Context HttpServletRequest request,
@PathParam("style") String styleId) {
Site site = getSite(request);
// Search styles of current site
for (Module m : site.getModules()) {
ImageStyle style = m.getImageStyle(styleId);
if (style != null) {
ResponseBuilder response = Response.ok(style.toXml());
return response.build();
}
}
// Search global styles
for (ImageStyle style : styles) {
if (style.getIdentifier().equals(styleId)) {
ResponseBuilder response = Response.ok(style.toXml());
return response.build();
}
}
// The image style was not found
throw new WebApplicationException(Status.NOT_FOUND);
}
/**
* Returns the endpoint documentation.
*
* @return the endpoint documentation
*/
@GET
@Path("/docs")
@Produces(MediaType.TEXT_HTML)
public String getDocumentation(@Context HttpServletRequest request) {
if (docs == null) {
String docsPath = request.getRequestURI();
String docsPathExtension = request.getPathInfo();
String servicePath = request.getRequestURI().substring(0, docsPath.length() - docsPathExtension.length());
docs = PreviewsEndpointDocs.createDocumentation(servicePath);
}
return docs;
}
/**
* Callback from OSGi declarative services on registration of a new image
* style in the service registry.
*
* @param style
* the image style
*/
void addImageStyle(ImageStyle style) {
styles.add(style);
}
/**
* Callback from OSGi declarative services on removal of an image style from
* the service registry.
*
* @param style
* the image style
*/
void removeImageStyle(ImageStyle style) {
styles.remove(style);
}
/**
* Callback from the OSGi environment when the environment becomes published.
*
* @param environment
* the environment
*/
void setEnvironment(Environment environment) {
this.environment = environment;
}
/**
* OSGi callback that is setting the resource serializer.
*
* @param serializer
* the resource serializer service
*/
void setResourceSerializer(ResourceSerializerService serializer) {
this.serializerService = serializer;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "Previews rest endpoint";
}
}