/*
* *
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* /
*/
package org.anhonesteffort.flock.webdav;
import org.anhonesteffort.flock.util.guava.Optional;
import org.anhonesteffort.flock.webdav.caldav.CalDavConstants;
import org.apache.jackrabbit.webdav.DavConstants;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.MultiStatus;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod;
import org.apache.jackrabbit.webdav.client.methods.ReportMethod;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
import org.apache.jackrabbit.webdav.property.DavPropertySet;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.security.SecurityConstants;
import org.apache.jackrabbit.webdav.version.report.ReportInfo;
import org.apache.jackrabbit.webdav.version.report.ReportType;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
/**
* Programmer: rhodey
*/
public abstract class AbstractDavComponentCollection<T> implements DavComponentCollection<T> {
private final AbstractDavComponentStore<?> store;
protected DavClient client;
private String path;
protected DavPropertySet properties;
protected AbstractDavComponentCollection(AbstractDavComponentStore<?> store,
String path) {
this.store = store;
this.client = store.davClient;
this.path = path;
properties = new DavPropertySet();
}
protected AbstractDavComponentCollection(AbstractDavComponentStore<?> store,
String path,
DavPropertySet properties)
{
this.store = store;
this.client = store.davClient;
this.path = path;
this.properties = properties;
}
protected AbstractDavComponentCollection(AbstractDavComponentStore<?> store,
String path,
String displayName)
{
this.store = store;
this.client = store.davClient;
this.path = path;
properties = new DavPropertySet();
properties.add(new DefaultDavProperty<String>(DavPropertyName.DISPLAYNAME, displayName));
}
public void setClient(DavClient client) {
this.client = client;
}
public AbstractDavComponentStore<?> getStore() {
return store;
}
@Override
public String getPath() {
if (!path.endsWith("/"))
path = path.concat("/");
return path;
}
protected abstract String getComponentPathFromUid(String uid);
protected Optional<String> getUidFromComponentPath(String path) {
int extension_index = path.lastIndexOf(".");
int file_name_index = path.lastIndexOf("/");
if (file_name_index == -1 || extension_index == -1 ||
file_name_index == (path.length() - 1))
return Optional.absent();
return Optional.of(path.substring(file_name_index + 1, extension_index));
}
protected DavPropertyNameSet getPropertyNamesForFetch() {
DavPropertyNameSet baseProperties = new DavPropertyNameSet();
baseProperties.add(DavPropertyName.RESOURCETYPE);
baseProperties.add(DavPropertyName.DISPLAYNAME);
baseProperties.add(SecurityConstants.OWNER);
baseProperties.add(WebDavConstants.PROPERTY_NAME_PROP);
baseProperties.add(SecurityConstants.CURRENT_USER_PRIVILEGE_SET);
baseProperties.add(WebDavConstants.PROPERTY_NAME_QUOTA_AVAILABLE_BYTES);
baseProperties.add(WebDavConstants.PROPERTY_NAME_QUOTA_USED_BYTES);
baseProperties.add(WebDavConstants.PROPERTY_NAME_RESOURCE_ID);
baseProperties.add(WebDavConstants.PROPERTY_NAME_SUPPORTED_REPORT_SET);
baseProperties.add(WebDavConstants.PROPERTY_NAME_SYNC_TOKEN);
baseProperties.add(CalDavConstants.PROPERTY_NAME_CTAG);
return baseProperties;
}
protected abstract ReportType getMultiGetReportType();
protected abstract ReportType getQueryReportType();
protected abstract DavPropertyNameSet getPropertyNamesForReports();
public DavPropertySet getProperties() {
return properties;
}
// TODO: make this cleaner?
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public <P> Optional<P> getProperty(DavPropertyName propertyName, Class<P> type)
throws PropertyParseException
{
if (properties.get(propertyName) != null) {
try {
Object value = properties.get(propertyName).getValue();
if (Collection.class.isAssignableFrom(type)) {
P result = type.newInstance();
if (value instanceof Collection<?>)
((Collection<?>) result).addAll((Collection) value);
return Optional.of(result);
}
else {
Constructor<P> constructor = type.getConstructor(value.getClass());
return Optional.of(constructor.newInstance(value));
}
} catch (Exception e) {
throw new PropertyParseException("caught exception while getting property " +
propertyName.getName(), path, propertyName, e);
}
}
return Optional.absent();
}
@Override
@SuppressWarnings("unchecked")
public List<String> getResourceTypes() throws PropertyParseException {
List<String> resourceTypes = new LinkedList<String>();
Optional<ArrayList> resourceTypeProp = getProperty(DavPropertyName.RESOURCETYPE, ArrayList.class);
if (!resourceTypeProp.isPresent())
return resourceTypes;
for (Node child : (ArrayList<Node>) resourceTypeProp.get()) {
if (child instanceof Element) {
String nameNode = child.getNodeName();
if (nameNode != null)
resourceTypes.add(nameNode);
}
}
return resourceTypes;
}
@Override
public Optional<String> getDisplayName() throws PropertyParseException {
return getProperty(DavPropertyName.DISPLAYNAME, String.class);
}
@Override
public Optional<String> getCTag() throws PropertyParseException {
return getProperty(CalDavConstants.PROPERTY_NAME_CTAG, String.class);
}
@Override
public void setDisplayName(String displayName) throws DavException, IOException {
DavPropertySet updateProperties = new DavPropertySet();
updateProperties.add(new DefaultDavProperty<String>(DavPropertyName.DISPLAYNAME, displayName));
patchProperties(updateProperties, new DavPropertyNameSet());
}
@Override
public Optional<String> getOwnerHref() throws PropertyParseException {
Optional<ArrayList> ownerProp = getProperty(SecurityConstants.OWNER, ArrayList.class);
if (ownerProp.isPresent()) {
for (Node child : (ArrayList<Node>) ownerProp.get()) {
if (child instanceof Element) {
String nameNode = child.getNodeName();
if ((nameNode != null) && (DavConstants.XML_HREF.equals(nameNode)))
return Optional.of(child.getTextContent());
}
}
}
return Optional.absent();
}
@Override
public Optional<Long> getQuotaAvailableBytes() throws PropertyParseException {
return getProperty(WebDavConstants.PROPERTY_NAME_QUOTA_AVAILABLE_BYTES, Long.class);
}
@Override
public Optional<Long> getQuotaUsedBytes() throws PropertyParseException {
return getProperty(WebDavConstants.PROPERTY_NAME_QUOTA_USED_BYTES, Long.class);
}
@Override
public Optional<String> getResourceId() throws PropertyParseException {
Optional<ArrayList> resourceIdProp = getProperty(WebDavConstants.PROPERTY_NAME_RESOURCE_ID, ArrayList.class);
if (resourceIdProp.isPresent()) {
for (Node child : (ArrayList<Node>) resourceIdProp.get()) {
if (child instanceof Element) {
String nameNode = child.getNodeName();
if ((nameNode != null) && (DavConstants.XML_HREF.equals(nameNode)))
return Optional.of(child.getTextContent());
}
}
}
return Optional.absent();
}
// TODO: make use of this in REPORT methods if present.
@Override
public Optional<String> getSyncToken() throws PropertyParseException {
return getProperty(WebDavConstants.PROPERTY_NAME_SYNC_TOKEN, String.class);
}
public void fetchProperties(DavPropertyNameSet fetchProps) throws DavException, IOException {
PropFindMethod propFindMethod = new PropFindMethod(getPath(), fetchProps, PropFindMethod.DEPTH_0);
try {
client.execute(propFindMethod);
if (propFindMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) {
MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus();
MultiStatusResponse[] responses = multiStatus.getResponses();
DavPropertySet foundProperties = responses[0].getProperties(WebDavConstants.SC_OK);
if (foundProperties != null)
properties = foundProperties;
}
else
throw new DavException(propFindMethod.getStatusCode(), propFindMethod.getStatusText());
} finally {
propFindMethod.releaseConnection();
}
}
public void fetchProperties() throws DavException, IOException {
fetchProperties(getPropertyNamesForFetch());
}
@Override
public void patchProperties(DavPropertySet setProperties, DavPropertyNameSet removeProperties)
throws DavException, IOException
{
PropPatchMethod propPatchMethod = new PropPatchMethod(getPath(), setProperties, removeProperties);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
propPatchMethod.getRequestEntity().writeRequest(stream);
client.execute(propPatchMethod);
if (propPatchMethod.getStatusCode() != DavServletResponse.SC_MULTI_STATUS)
throw new DavException(propPatchMethod.getStatusCode(), propPatchMethod.getStatusText());
fetchProperties();
} finally {
propPatchMethod.releaseConnection();
}
}
@Override
public HashMap<String, String> getComponentETags() throws DavException, IOException {
HashMap<String, String> componentETagPairs = new HashMap<String, String>();
DavPropertyNameSet fetchProps = new DavPropertyNameSet();
fetchProps.add(DavPropertyName.GETETAG);
PropFindMethod propFindMethod = new PropFindMethod(getPath(), fetchProps, PropFindMethod.DEPTH_1);
try {
client.execute(propFindMethod);
if (propFindMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS) {
MultiStatus multiStatus = propFindMethod.getResponseBodyAsMultiStatus();
MultiStatusResponse[] responses = multiStatus.getResponses();
for (MultiStatusResponse msResponse : responses) {
DavPropertySet foundProperties = msResponse.getProperties(WebDavConstants.SC_OK);
Optional<String> componentUid = getUidFromComponentPath(msResponse.getHref());
if (componentUid.isPresent() && foundProperties.get(DavPropertyName.PROPERTY_GETETAG) != null) {
DavProperty getETagProp = foundProperties.get(DavPropertyName.PROPERTY_GETETAG);
componentETagPairs.put(componentUid.get(), (String) getETagProp.getValue());
}
}
}
else
throw new DavException(propFindMethod.getStatusCode(),
"PROPFIND response not multi-status, response: " + propFindMethod.getStatusLine());
} finally {
propFindMethod.releaseConnection();
}
return componentETagPairs;
}
protected abstract MultiStatusResult<T> getComponentsFromMultiStatus(MultiStatusResponse[] msResponses);
@Override
public MultiStatusResult<T> getComponents(List<String> uids)
throws DavException, IOException
{
ReportInfo reportInfo = new ReportInfo(getMultiGetReportType(), 1, getPropertyNamesForReports());
try {
if (uids.size() > 0) {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
for (String uid : uids) {
Element componentHREF = DomUtil.createElement(document, DavConstants.XML_HREF, DavConstants.NAMESPACE);
componentHREF.setTextContent(getComponentPathFromUid(uid));
reportInfo.setContentElement(componentHREF);
}
}
ReportMethod reportMethod = new ReportMethod(getPath(), reportInfo);
try {
client.execute(reportMethod);
if (reportMethod.getStatusCode() == DavServletResponse.SC_MULTI_STATUS)
return getComponentsFromMultiStatus(reportMethod.getResponseBodyAsMultiStatus().getResponses());
else if (reportMethod.getStatusCode() == WebDavConstants.SC_NOT_FOUND)
return new MultiStatusResult<T>(new LinkedList<ComponentETagPair<T>>(), new LinkedList<InvalidComponentException>());
else
throw new DavException(reportMethod.getStatusCode(), reportMethod.getStatusText());
} finally {
reportMethod.releaseConnection();
}
} catch (ParserConfigurationException e) {
throw new IOException("Caught exception while building document.", e);
}
}
@Override
public Optional<ComponentETagPair<T>> getComponent(String uid)
throws InvalidComponentException, DavException, IOException
{
List<String> uids = new LinkedList<String>();
uids.add(uid);
MultiStatusResult<T> multiStatusResult = getComponents(uids);
if (multiStatusResult.getInvalidComponentExceptions().size() > 0)
throw multiStatusResult.getInvalidComponentExceptions().get(0);
if (multiStatusResult.getComponentETagPairs().isEmpty())
return Optional.absent();
return Optional.of(multiStatusResult.getComponentETagPairs().get(0));
}
@Override
public MultiStatusResult<T> getComponents()
throws DavException, IOException
{
return getComponents(new LinkedList<String>());
}
protected abstract void putComponentToServer(T calendar, Optional<String> ifMatchETag)
throws DavException, IOException, InvalidComponentException;
@Override
public void addComponent(T component)
throws InvalidComponentException, DavException, IOException
{
putComponentToServer(component, Optional.<String>absent());
}
@Override
public void updateComponent(ComponentETagPair<T> component)
throws InvalidComponentException, DavException, IOException
{
putComponentToServer(component.getComponent(), component.getETag());
}
@Override
public void removeComponent(String path) throws DavException, IOException {
DeleteMethod deleteMethod = new DeleteMethod(getComponentPathFromUid(path));
try {
client.execute(deleteMethod);
if (!deleteMethod.succeeded() && deleteMethod.getStatusCode() != WebDavConstants.SC_OK)
throw new DavException(deleteMethod.getStatusCode(), deleteMethod.getStatusText());
} finally {
deleteMethod.releaseConnection();
}
}
}