/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 io.milton.http.webdav; import io.milton.common.NameSpace; import io.milton.http.*; import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.NotAuthorizedException; import io.milton.http.exceptions.NotFoundException; import io.milton.http.http11.CustomPostHandler; import io.milton.http.http11.ETagGenerator; import io.milton.http.quota.QuotaDataAccessor; import io.milton.http.report.QualifiedReport; import io.milton.http.report.Report; import io.milton.http.report.ReportHandler; import io.milton.http.values.SupportedReportSetList; import io.milton.http.values.ValueWriters; import io.milton.http.webdav.PropertyMap.StandardProperty; import io.milton.http.webdav.PropertyMap.WritableStandardProperty; import io.milton.property.PropertyAuthoriser; import io.milton.property.PropertySource; import io.milton.resource.CollectionResource; import io.milton.resource.DisplayNameResource; import io.milton.resource.GetableResource; import io.milton.resource.PropFindableResource; import io.milton.resource.PutableResource; import io.milton.resource.Resource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.xml.namespace.QName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Defines the methods and properties that make up the webdav protocol. * * We've been a little pragmatic about what the webdav protocol actually is. It * generally doesnt include things defined in subsequent protocols (RFC's), but * where something is frequently used by other protocols (like REPORT) or is * very tightly couple with normal webdav operations (like quota checking) * you'll find it here * * * @author brad */ public class WebDavProtocol implements HttpExtension, PropertySource { private static final Logger log = LoggerFactory.getLogger(WebDavProtocol.class); public static final String DAV_URI = "DAV:"; public static final String DAV_PREFIX = "d"; public static final NameSpace NS_DAV = new NameSpace(DAV_URI, DAV_PREFIX); private final Set<Handler> handlers; private final Map<String, Report> reports; private final ResourceTypeHelper resourceTypeHelper; private final QuotaDataAccessor quotaDataAccessor; private final PropertyMap propertyMap; private final List<PropertySource> propertySources; private final ETagGenerator eTagGenerator; private final HandlerHelper handlerHelper; private final UserAgentHelper userAgentHelper; private final DisplayNameFormatter displayNameFormatter; private final MkColHandler mkColHandler; private final PropPatchHandler propPatchHandler; private List<CustomPostHandler> customPostHandlers; public WebDavProtocol(HandlerHelper handlerHelper, ResourceTypeHelper resourceTypeHelper, WebDavResponseHandler responseHandler, List<PropertySource> propertySources, QuotaDataAccessor quotaDataAccessor, PropPatchSetter patchSetter, PropertyAuthoriser propertyAuthoriser, ETagGenerator eTagGenerator, UrlAdapter urlAdapter, ResourceHandlerHelper resourceHandlerHelper, UserAgentHelper userAgentHelper, PropFindRequestFieldParser requestFieldParser, PropFindPropertyBuilder propertyBuilder, DisplayNameFormatter displayNameFormatter, boolean enableTextContentProperty) { this.displayNameFormatter = displayNameFormatter; this.userAgentHelper = userAgentHelper; this.handlerHelper = handlerHelper; this.eTagGenerator = eTagGenerator; handlers = new HashSet<Handler>(); this.resourceTypeHelper = resourceTypeHelper; this.quotaDataAccessor = quotaDataAccessor; this.propertyMap = new PropertyMap(WebDavProtocol.NS_DAV.getName()); propertyMap.add(new ContentLengthPropertyWriter()); propertyMap.add(new ContentTypePropertyWriter()); propertyMap.add(new CreationDatePropertyWriter("getcreated")); propertyMap.add(new CreationDatePropertyWriter("creationdate")); propertyMap.add(new DisplayNamePropertyWriter()); propertyMap.add(new LastModifiedDatePropertyWriter()); propertyMap.add(new ResourceTypePropertyWriter()); propertyMap.add(new EtagPropertyWriter()); propertyMap.add(new MSIsCollectionPropertyWriter()); propertyMap.add(new MSIsReadOnlyPropertyWriter()); propertyMap.add(new MSNamePropertyWriter()); log.info("resourceTypeHelper: " + resourceTypeHelper.getClass()); if (quotaDataAccessor == null) { log.info("no quota data"); } else { log.info("quotaDataAccessor: " + quotaDataAccessor.getClass()); propertyMap.add(new QuotaAvailableBytesPropertyWriter()); propertyMap.add(new QuotaUsedBytesPropertyWriter()); } propertyMap.add(new SupportedReportSetProperty()); if (enableTextContentProperty) { propertyMap.add(new MiltonExtTextContentProperty()); } // note valuewriters is also used in DefaultWebDavResponseHandler // if using non-default configuration you should inject the same instance into there // and here ValueWriters valueWriters = new ValueWriters(); if (propertySources == null) { propertySources = new ArrayList<PropertySource>(); } log.debug("provided property sources: " + propertySources.size()); this.propertySources = propertySources; log.debug("adding webdav as a property source to: " + this.propertySources.getClass() + " hashCode: " + this.propertySources.hashCode()); addPropertySource(this); if (patchSetter == null) { log.info("creating default patcheSetter: " + PropertySourcePatchSetter.class); patchSetter = new PropertySourcePatchSetter(propertySources, valueWriters); } //handlers.add(new PropFindHandler(resourceHandlerHelper, resourceTypeHelper, responseHandler, propertySources)); PropFindHandler propFindHandler = new PropFindHandler(resourceHandlerHelper, requestFieldParser, responseHandler, propertyBuilder); handlers.add(propFindHandler); mkColHandler = new MkColHandler(responseHandler, handlerHelper); handlers.add(mkColHandler); propPatchHandler = new PropPatchHandler(resourceHandlerHelper, new DefaultPropPatchParser(), patchSetter, responseHandler, propertyAuthoriser); handlers.add(propPatchHandler); handlers.add(new CopyHandler(responseHandler, handlerHelper, resourceHandlerHelper, userAgentHelper)); handlers.add(new MoveHandler(responseHandler, handlerHelper, resourceHandlerHelper, userAgentHelper)); // Reports are added by other protocols via addReport reports = new HashMap<String, Report>(); handlers.add(new ReportHandler(responseHandler, resourceHandlerHelper, reports)); } @Override public List<CustomPostHandler> getCustomPostHandlers() { return customPostHandlers; } public void setCustomPostHandlers(List<CustomPostHandler> customPostHandlers) { this.customPostHandlers = customPostHandlers; } public List<PropertySource> getPropertySources() { return Collections.unmodifiableList(propertySources); } public void addPropertySource(PropertySource ps) { propertySources.add(ps); log.debug("adding property source: " + ps.getClass() + " new size: " + propertySources.size()); } public void addReport(Report report) { this.reports.put(report.getName(), report); } @Override public Set<Handler> getHandlers() { return Collections.unmodifiableSet(handlers); } @Override public Object getProperty(QName name, Resource r) { Object o = propertyMap.getProperty(name, r); return o; } @Override public void setProperty(QName name, Object value, Resource r) { throw new UnsupportedOperationException("Not supported. Standard webdav properties are not writable"); } @Override public PropertyMetaData getPropertyMetaData(QName name, Resource r) { PropertyMetaData propertyMetaData = propertyMap.getPropertyMetaData(name, r); if (propertyMetaData != null) { // Nautilus (at least on Ubuntu 12) doesnt like empty properties if (userAgentHelper.isNautilus(HttpManager.request())) { Object v = getProperty(name, r); if (v == null) { return PropertyMetaData.UNKNOWN; } else if (v instanceof String) { String s = (String) v; if (s.trim().length() == 0) { return PropertyMetaData.UNKNOWN; } } } } return propertyMetaData; } @Override public void clearProperty(QName name, Resource r) { throw new UnsupportedOperationException("Not supported. Standard webdav properties are not writable"); } @Override public List<QName> getAllPropertyNames(Resource r) { return propertyMap.getAllPropertyNames(r); } /** * Generates the displayname element text. By default is a * CdataDisplayNameFormatter wrapping a DefaultDisplayNameFormatter so that * extended character sets are supported * * @return */ public DisplayNameFormatter getDisplayNameFormatter() { return displayNameFormatter; } class DisplayNamePropertyWriter implements WritableStandardProperty<String> { @Override public String getValue(PropFindableResource res) { if( res instanceof DisplayNameResource) { DisplayNameResource dnr = (DisplayNameResource) res; return dnr.getDisplayName(); } return displayNameFormatter.formatDisplayName(res); } @Override public String fieldName() { return "displayname"; } @Override public Class<String> getValueClass() { return String.class; } @Override public void setValue(PropFindableResource res, String value) { if( res instanceof DisplayNameResource) { DisplayNameResource dnr = (DisplayNameResource) res; dnr.setDisplayName(value); } else { log.warn("Attempt to set displayname property, but resource is not compatible: " + res.getClass()); } } } class CreationDatePropertyWriter implements StandardProperty<Date> { private final String fieldName; public CreationDatePropertyWriter(String fieldName) { this.fieldName = fieldName; } @Override public String fieldName() { return fieldName; } @Override public Date getValue(PropFindableResource res) { // BM: was getModifiedDate(), presume that was wrong?? return res.getCreateDate(); } @Override public Class<Date> getValueClass() { return Date.class; } } class LastModifiedDatePropertyWriter implements StandardProperty<Date> { @Override public String fieldName() { return "getlastmodified"; } @Override public Date getValue(PropFindableResource res) { return res.getModifiedDate(); } @Override public Class<Date> getValueClass() { return Date.class; } } class ResourceTypePropertyWriter implements StandardProperty<List<QName>> { @Override public List<QName> getValue(PropFindableResource res) { return resourceTypeHelper.getResourceTypes(res); } @Override public String fieldName() { return "resourcetype"; } @Override public Class getValueClass() { return List.class; } } class ContentTypePropertyWriter implements StandardProperty<String> { @Override public String getValue(PropFindableResource res) { if (res instanceof GetableResource) { GetableResource getable = (GetableResource) res; String s = getable.getContentType(null); return s; } else { return ""; } } @Override public String fieldName() { return "getcontenttype"; } @Override public Class getValueClass() { return String.class; } } class ContentLengthPropertyWriter implements StandardProperty<Long> { @Override public Long getValue(PropFindableResource res) { if (res instanceof GetableResource) { GetableResource getable = (GetableResource) res; Long l = getable.getContentLength(); return l; } else { return null; } } @Override public String fieldName() { return "getcontentlength"; } @Override public Class getValueClass() { return Long.class; } } class QuotaUsedBytesPropertyWriter implements StandardProperty<Long> { @Override public Long getValue(PropFindableResource res) { if (quotaDataAccessor != null) { return quotaDataAccessor.getQuotaUsed(res); } else { return null; } } @Override public String fieldName() { return "quota-used-bytes"; } @Override public Class getValueClass() { return Long.class; } } class QuotaAvailableBytesPropertyWriter implements StandardProperty<Long> { @Override public Long getValue(PropFindableResource res) { if (quotaDataAccessor != null) { return quotaDataAccessor.getQuotaAvailable(res); } else { return null; } } @Override public String fieldName() { return "quota-available-bytes"; } @Override public Class getValueClass() { return Long.class; } } class EtagPropertyWriter implements StandardProperty<String> { @Override public String getValue(PropFindableResource res) { String etag = eTagGenerator.generateEtag(res); return etag; } @Override public String fieldName() { return "getetag"; } @Override public Class getValueClass() { return String.class; } } // MS specific fields class MSNamePropertyWriter extends DisplayNamePropertyWriter { @Override public String fieldName() { return "name"; } } class MSIsCollectionPropertyWriter implements StandardProperty<Boolean> { @Override public String fieldName() { return "iscollection"; } @Override public Boolean getValue(PropFindableResource res) { return (res instanceof CollectionResource); } @Override public Class getValueClass() { return Boolean.class; } } class MSIsReadOnlyPropertyWriter implements StandardProperty<Boolean> { @Override public String fieldName() { return "isreadonly"; } @Override public Boolean getValue(PropFindableResource res) { return !(res instanceof PutableResource); } @Override public Class getValueClass() { return Boolean.class; } } class SupportedReportSetProperty implements StandardProperty<SupportedReportSetList> { @Override public String fieldName() { return "supported-report-set"; } @Override public SupportedReportSetList getValue(PropFindableResource res) { SupportedReportSetList reportSet = new SupportedReportSetList(); for (Report report: reports.values()) { if(report instanceof QualifiedReport) reportSet.add(((QualifiedReport) report).getQualifiedName()); else reportSet.add(new QName(DAV_URI, report.getName())); } return reportSet; } @Override public Class getValueClass() { return SupportedReportSetList.class; } } class MiltonExtTextContentProperty implements StandardProperty<String> { @Override public String fieldName() { return "textcontent"; } @Override public String getValue(PropFindableResource res) { if (res instanceof GetableResource) { GetableResource gr = (GetableResource) res; String ct = gr.getContentType("text"); if (ct != null && ct.startsWith("text")) { ByteArrayOutputStream bout = new ByteArrayOutputStream(); try { gr.sendContent(bout, null, Collections.EMPTY_MAP, ct); return bout.toString("UTF-8"); } catch (IOException e) { throw new RuntimeException(e); } catch (NotAuthorizedException e) { return null; } catch (BadRequestException e) { return null; } catch (NotFoundException e) { return null; } } } return null; } @Override public Class getValueClass() { return String.class; } } protected void sendStringProp(XmlWriter writer, String name, String value) { String s = value; if (s == null) { writer.writeProperty(null, name); } else { writer.writeProperty(null, name, s); } } void sendDateProp(XmlWriter writer, String name, Date date) { sendStringProp(writer, name, (date == null ? null : DateUtils.formatDate(date))); } public HandlerHelper getHandlerHelper() { return handlerHelper; } public QuotaDataAccessor getQuotaDataAccessor() { return quotaDataAccessor; } public Map<String, Report> getReports() { return reports; } public ResourceTypeHelper getResourceTypeHelper() { return resourceTypeHelper; } public ETagGenerator geteTagGenerator() { return eTagGenerator; } public PropertyMap getPropertyMap() { return propertyMap; } public MkColHandler getMkColHandler() { return mkColHandler; } public PropPatchHandler getPropPatchHandler() { return propPatchHandler; } }