/*
Copyright 2011-2014 Red Hat, Inc
This file is part of PressGang CCMS.
PressGang CCMS 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 3 of the License, or
(at your option) any later version.
PressGang CCMS 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 PressGang CCMS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jboss.pressgang.ccms.server.webdav.resources;
import static javax.ws.rs.core.Response.Status.OK;
import static net.java.dev.webdav.jaxrs.xml.properties.ResourceType.COLLECTION;
import javax.validation.constraints.NotNull;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.java.dev.webdav.jaxrs.xml.elements.HRef;
import net.java.dev.webdav.jaxrs.xml.elements.Prop;
import net.java.dev.webdav.jaxrs.xml.elements.PropStat;
import net.java.dev.webdav.jaxrs.xml.elements.Status;
import org.apache.commons.io.IOUtils;
import org.jboss.pressgang.ccms.server.webdav.managers.CompatibilityManager;
import org.jboss.pressgang.ccms.server.webdav.resources.hierarchy.InternalResourceRoot;
import org.jboss.pressgang.ccms.server.webdav.resources.hierarchy.topics.InternalResourceTopicVirtualFolder;
import org.jboss.pressgang.ccms.server.webdav.resources.hierarchy.topics.topic.InternalResourceTopic;
import org.jboss.pressgang.ccms.server.webdav.resources.hierarchy.topics.topic.fields.InternalResourceTempTopicFile;
import org.jboss.pressgang.ccms.server.webdav.resources.hierarchy.topics.topic.fields.InternalResourceTopicContent;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The WebDAV server exposes resources from multiple locations. Some resources are found in a database, and some
* are saved as files.
* <p/>
* All resources can potentially be written to, read and deleted. Copying and moving are just combinations of this
* basic functionality.
* <p/>
* Instances of the InternalResource class wrap the functionality needed to read, write and delete.
* <p/>
* The InternalResource class is also a factory, matching url paths to the InternalResource instances that manage
* them. This provides a simple way for the JAX-RS interface to pass off the actual implementation of these underlying
* methods.
* <p/>
* This means that the WebDavResource class can defer functionality to InternalResource.
*/
public abstract class InternalResource {
private static final Logger LOGGER = LoggerFactory.getLogger(InternalResource.class.getName());
/**
* Matches something like /webdav/TOPICS/3/4/5/6/TOPIC3456/
*/
public static final Pattern TOPIC_RE = Pattern.compile("/webdav/TOPICS(?<var>(/\\d)*)/TOPIC(?<TopicID>\\d*)/?");
/**
* Matches the root directory /webdav
*/
public static final Pattern ROOT_FOLDER_RE = Pattern.compile("/webdav/?");
/**
* Matches something like /webdav/TOPICS/3/4/5/6/ or /TOPICS
*/
public static final Pattern TOPIC_FOLDER_RE = Pattern.compile("/webdav/TOPICS(?<var>(/\\d)*)/?");
/**
* Matches something like /webdav/TOPICS/3/4/5/6/TOPIC3456/3456.xml
*/
public static final Pattern TOPIC_CONTENTS_RE = Pattern.compile("/webdav/TOPICS(/\\d)*/TOPIC(?<TopicID>\\d+)/\\k<TopicID>.xml");
/**
* Matches something like /webdav/TOPICS/3/4/5/6/TOPIC3456/3456.xml~
*/
public static final Pattern TOPIC_TEMP_FILE_RE = Pattern.compile("/webdav/TOPICS(/\\d)*/TOPIC\\d+/[^/]+");
/**
* The integer id that this resource represents. This is usually a database primary key. This or stringId will
* be not null.
*/
@Nullable
private final Integer intId;
/**
* The integer id that this resource represents. This is usally a filename. This or intId will
* be not null.
*/
@Nullable
private final String stringId;
/**
* Info about the request.
*/
@Nullable
private final UriInfo uriInfo;
/**
* The client id
*/
@NotNull
private final String remoteAddress;
/**
* The manager responsible for determining if a resource is deleted or not
*/
@NotNull private final CompatibilityManager compatibilityManager;
protected InternalResource(@Nullable final UriInfo uriInfo, @NotNull final CompatibilityManager compatibilityManager,
@NotNull final String remoteAddress, @NotNull final Integer intId) {
this.intId = intId;
this.stringId = null;
this.uriInfo = uriInfo;
this.remoteAddress = remoteAddress;
this.compatibilityManager = compatibilityManager;
}
protected InternalResource(@Nullable final UriInfo uriInfo, @NotNull final CompatibilityManager compatibilityManager,
@NotNull final String remoteAddress, @NotNull final String stringId) {
this.intId = null;
this.stringId = stringId;
this.uriInfo = uriInfo;
this.remoteAddress = remoteAddress;
this.compatibilityManager = compatibilityManager;
}
public int write(@NotNull final byte[] contents) {
throw new UnsupportedOperationException();
}
public int delete() {
throw new UnsupportedOperationException();
}
public ByteArrayReturnValue get() {
throw new UnsupportedOperationException();
}
public MultiStatusReturnValue propfind(final String depth) {
throw new UnsupportedOperationException();
}
public static javax.ws.rs.core.Response propfind(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo, final String depth) {
LOGGER.debug("ENTER InternalResource.propfind() " + uriInfo.getPath() + " " + depth + " " + remoteAddress);
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (sourceResource != null) {
final MultiStatusReturnValue multiStatusReturnValue = sourceResource.propfind(depth);
if (multiStatusReturnValue.getStatusCode() != 207) {
return javax.ws.rs.core.Response.status(multiStatusReturnValue.getStatusCode()).build();
}
return javax.ws.rs.core.Response.status(207).entity(multiStatusReturnValue.getValue()).type(MediaType.TEXT_XML).build();
}
return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.NOT_FOUND).build();
}
public static javax.ws.rs.core.Response copy(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo, @NotNull final String overwriteStr, @NotNull final String destination) {
LOGGER.debug("ENTER InternalResource.copy() " + uriInfo.getPath() + " " + destination + " " + remoteAddress);
try {
final HRef destHRef = new HRef(destination);
final URI destUriInfo = destHRef.getURI();
final InternalResource destinationResource = InternalResource.getInternalResource(null, compatibilityManager, remoteAddress,
destUriInfo.getPath());
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (destinationResource != null && sourceResource != null) {
final ByteArrayReturnValue byteArrayReturnValue = sourceResource.get();
if (byteArrayReturnValue.getStatusCode() != javax.ws.rs.core.Response.Status.OK.getStatusCode()) {
return javax.ws.rs.core.Response.status(byteArrayReturnValue.getStatusCode()).build();
}
int statusCode;
if ((statusCode = destinationResource.write(
byteArrayReturnValue.getValue())) != javax.ws.rs.core.Response.Status.NO_CONTENT.getStatusCode()) {
return javax.ws.rs.core.Response.status(statusCode).build();
}
return javax.ws.rs.core.Response.ok().build();
}
} catch (final URISyntaxException e) {
}
return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.NOT_FOUND).build();
}
public static javax.ws.rs.core.Response move(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo, @NotNull final String overwriteStr, @NotNull final String destination) {
LOGGER.debug("ENTER InternalResource.move() " + uriInfo.getPath() + " " + destination + " " + remoteAddress);
// We can't move outside of the filesystem
if (!destination.startsWith(uriInfo.getBaseUri().toString())) {
return javax.ws.rs.core.Response.status(Response.Status.NOT_FOUND).build();
}
final InternalResource destinationResource = InternalResource.getInternalResource(null, compatibilityManager, remoteAddress,
"/" + destination.replaceFirst(uriInfo.getBaseUri().toString(), ""));
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (destinationResource != null && sourceResource != null) {
final ByteArrayReturnValue byteArrayReturnValue = sourceResource.get();
if (byteArrayReturnValue.getStatusCode() != javax.ws.rs.core.Response.Status.OK.getStatusCode()) {
return javax.ws.rs.core.Response.status(byteArrayReturnValue.getStatusCode()).build();
}
int statusCode;
if ((statusCode = destinationResource.write(
byteArrayReturnValue.getValue())) != javax.ws.rs.core.Response.Status.NO_CONTENT.getStatusCode()) {
return javax.ws.rs.core.Response.status(statusCode).build();
}
if ((statusCode = sourceResource.delete()) != javax.ws.rs.core.Response.Status.NO_CONTENT.getStatusCode()) {
return javax.ws.rs.core.Response.status(statusCode).build();
}
return javax.ws.rs.core.Response.ok().build();
}
return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.NOT_FOUND).build();
}
public static javax.ws.rs.core.Response delete(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo) {
LOGGER.debug("ENTER InternalResource.delete() " + uriInfo.getPath() + " " + remoteAddress);
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (sourceResource != null) {
return javax.ws.rs.core.Response.status(sourceResource.delete()).build();
}
return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.NOT_FOUND).build();
}
public static ByteArrayReturnValue get(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo) {
LOGGER.debug("ENTER InternalResource.get() " + uriInfo.getPath() + " " + remoteAddress);
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (sourceResource != null) {
ByteArrayReturnValue statusCode;
if ((statusCode = sourceResource.get()).getStatusCode() != javax.ws.rs.core.Response.Status.OK.getStatusCode()) {
return statusCode;
}
return statusCode;
}
return new ByteArrayReturnValue(javax.ws.rs.core.Response.Status.NOT_FOUND.getStatusCode(), null);
}
public static javax.ws.rs.core.Response put(@NotNull final CompatibilityManager compatibilityManager, @NotNull final String remoteAddress,
@NotNull final UriInfo uriInfo, @NotNull final InputStream entityStream) {
LOGGER.debug("ENTER InternalResource.put() " + uriInfo.getPath() + " " + remoteAddress);
try {
final InternalResource sourceResource = InternalResource.getInternalResource(uriInfo, compatibilityManager, remoteAddress,
uriInfo.getPath());
if (sourceResource != null) {
final byte[] data = IOUtils.toByteArray(entityStream);
int statusCode = sourceResource.write(data);
return javax.ws.rs.core.Response.status(statusCode).build();
}
return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.NOT_FOUND).build();
} catch (final IOException e) {
}
return javax.ws.rs.core.Response.serverError().build();
}
/**
* The factory method that returns the object to handle a URL request.
*
* @param uri The request URI
* @return The object to handle the response, or null if the URL is invalid.
*/
public static InternalResource getInternalResource(@Nullable final UriInfo uri, @NotNull final CompatibilityManager compatibilityManager,
@NotNull final String remoteAddress, @NotNull final String requestPath) {
LOGGER.debug("ENTER InternalResource.getInternalResource() " + requestPath);
/*
* Order is important here, as the TOPIC_TEMP_FILE_RE will match everything the TOPIC_CONTENTS_RE will. So we check
* TOPIC_TEMP_FILE_RE after TOPIC_CONTENTS_RE.
*
* Other regexes are specific enough not to match each other.
*/
final Matcher topicContents = TOPIC_CONTENTS_RE.matcher(requestPath);
if (topicContents.matches()) {
LOGGER.debug("Matched InternalResourceTopicContent");
return new InternalResourceTopicContent(uri, compatibilityManager, remoteAddress, Integer.parseInt(topicContents.group("TopicID")));
}
final Matcher topicFolder = TOPIC_FOLDER_RE.matcher(requestPath);
if (topicFolder.matches()) {
LOGGER.debug("Matched InternalResourceTopicVirtualFolder");
return new InternalResourceTopicVirtualFolder(uri, compatibilityManager, remoteAddress, requestPath);
}
final Matcher rootFolder = ROOT_FOLDER_RE.matcher(requestPath);
if (rootFolder.matches()) {
LOGGER.debug("Matched InternalResourceRoot");
return new InternalResourceRoot(uri, compatibilityManager, remoteAddress, requestPath);
}
final Matcher topic = TOPIC_RE.matcher(requestPath);
if (topic.matches()) {
LOGGER.debug("Matched InternalResourceTopic");
return new InternalResourceTopic(uri, compatibilityManager, remoteAddress, Integer.parseInt(topic.group("TopicID")));
}
final Matcher topicTemp = TOPIC_TEMP_FILE_RE.matcher(requestPath);
if (topicTemp.matches()) {
LOGGER.debug("Matched InternalResourceTempTopicFile");
return new InternalResourceTempTopicFile(uri, compatibilityManager, remoteAddress, requestPath);
}
LOGGER.debug("None matched");
return null;
}
/**
* Returning a child folder means returning a Respose that identifies a WebDAV collection.
* This method populates the returned request with the information required to identify
* a child folder.
*
* @param uriInfo The URI of the current request
* @param resourceName The name of the child folder
* @return The properties for a child folder
*/
public static net.java.dev.webdav.jaxrs.xml.elements.Response getFolderProperties(@NotNull final UriInfo uriInfo,
@NotNull final String resourceName) {
/*final Date lastModified = new Date(0);
final CreationDate creationDate = new CreationDate(lastModified);
final GetLastModified getLastModified = new GetLastModified(lastModified);
final Prop prop = new Prop(creationDate, getLastModified, COLLECTION);*/
final Prop prop = new Prop(COLLECTION);
final Status status = new Status((javax.ws.rs.core.Response.StatusType) OK);
final PropStat propStat = new PropStat(prop, status);
final URI uri = uriInfo.getRequestUriBuilder().path(resourceName).build();
final HRef hRef = new HRef(uri);
final net.java.dev.webdav.jaxrs.xml.elements.Response folder = new net.java.dev.webdav.jaxrs.xml.elements.Response(hRef, null, null,
null, propStat);
return folder;
}
/**
* @param uriInfo The URI of the current request
* @return The properties for the current folder
*/
public static net.java.dev.webdav.jaxrs.xml.elements.Response getFolderProperties(@NotNull final UriInfo uriInfo) {
/*final Date lastModified = new Date(0);
final CreationDate creationDate = new CreationDate(lastModified);
final GetLastModified getLastModified = new GetLastModified(lastModified);
final Prop prop = new Prop(creationDate, getLastModified, COLLECTION);*/
final Prop prop = new Prop(COLLECTION);
final Status status = new Status((javax.ws.rs.core.Response.StatusType) OK);
final PropStat propStat = new PropStat(prop, status);
final URI uri = uriInfo.getRequestUri();
final HRef hRef = new HRef(uri);
final net.java.dev.webdav.jaxrs.xml.elements.Response folder = new net.java.dev.webdav.jaxrs.xml.elements.Response(hRef, null, null,
null, propStat);
return folder;
}
/**
* The info about the request that was used to retrieve this object. This can be null
* when the initial request results in a second resource object being looked up (copy and move).
* <p/>
* All resource objects should check to make sure this is not null when doing a propfind (which is
* where the uriInfo is actually used). However, there should never be a case where a secondary
* resource object has its propfind method called.
*/
@Nullable
public UriInfo getUriInfo() {
return uriInfo;
}
/**
* The id of the entity that this resource represents. Usually a database primary key,
* but the context of the ID is up to the resource class.
*/
@Nullable
public Integer getIntId() {
return intId;
}
/**
* The id of the entity that this resource represents. Usually a file name,
* but the context of the ID is up to the resource class.
*/
@Nullable
public String getStringId() {
return stringId;
}
@NotNull
public String getRemoteAddress() {
return remoteAddress;
}
@NotNull
public CompatibilityManager getCompatibilityManager() {
return compatibilityManager;
}
}