Creating Class 1 / Class 3 WebDAV Server in Java

Class 1 / Class 3 WebDAV Server provides basic file management features available in every WebDAV server. On a Class 1 / Class 3 server, you can create files, copy, move and deleting files and folders as well as create, read and delete custom properties for each file or folder. Note that many WebDAV clients, such as Microsoft Office applications, Microsoft Web Folders client and Mac OS X WebDAV client require Class 2 server.

Framework Workflow

To create basic WebDAV server, you must implement File and Folder interfaces and create your class derived from Engine class. Also, you must implement HttpServlet and create your class derived from Engine in every request. Here is a typical workflow for each request:

  1. Create an instance of your class derived from Engine in each call to HttpServlet.service method.
  2. Call Engine.service() method providing the engine with HttpServletRequest and HttpServletResponse instances.
  3. Engine calls Engine.getLicense() and Engine.getLogger().
  4. Engine calls Engine.getHierarchyItem().
  5. Engine calls members of interfaces File, Folder, etc.

Sample HttpServlet implementation: 

public class MyWebDavServlet extends HttpServlet {

   @Override
   public void init(ServletConfig servletConfig) throws ServletException {
       super.init(servletConfig);
   } 

   @Override
   protected void service(HttpServletRequest httpServletRequest,

       HttpServletResponse httpServletResponse) throws ServletException, IOException {

       MyWebDavEngine engine = new MyWebDavEngine();
       engine.service(httpServletRequest, httpServletResponse);
   }
}

The Engine class is the core of IT Hit WebDAV framework, it parses WebDAV requests and generates responses. To provide a framework with information about items in your storage, you must implement Engine.getHierarchyItem method. The framework calls your method implementation passing context path of the file or folder as a parameter. In this method, you mast search in your repository for item requested and return it to the Engine:

public class WebDavEngine extends Engine {
   @Override
   public HierarchyItem getHierarchyItem(String path) throws ServerException {
       if (path.equals("/")) {
           return readItem("SELECT ID, Parent, ItemType, Name, Created, Modified"
                   + " FROM Repository"
                   + " WHERE ID = 0", path, false);
       } else {
           int id = 0;
           String[] names = path.split("/");
           int last = names.length - 1;
           while (last > 0 && names[last].equals("")) last--;
           // search for item by path
           for (int i = 0; i < last; i++)
               if (!names[i].equals("")) {
                   Integer res = executeInt("SELECT ID FROM Repository"
                           + " WHERE Name = ?"
                           + " AND Parent = ?", names[i], id);
                   if (res == null)
                       return null;
                   else
                       id = res;
               }
           // get item
           return readItem("SELECT ID, Parent, ItemType,Name,Created,Modified"
                   + " FROM Repository"
                   + " WHERE Name = ?"
                   + " AND Parent = ?", path, false, names[last], id);

       }
   }
}

The contextPath parameter is relative to WebDAV server root. For example, if your server root is located at ‘http://webdavserver:8080/dav/'and the requested item URL is ‘http://webdavserver:8080/dav/folder/item.doc’ this parameter will be ‘/folder/item.doc’.

Hierarchy Items

There are 2 types of items in a Class 1 WebDAV repository: folders, represented by Folder interface and files represented by File interface. Both Folder and File interfaces are derived from HierarchyItem interface. Most of HierarchyItem interface members are self explanatory and represent properties that each folder and file has, such as name of the item, item path, creation and modification date and methods for manipulating the item such as copyTo(), moveTo() and delete() methods.

The path that you return from your HierarchyItem.getPath() method implementation should be relative to WebDAV root. For example if your WebDAV server root is located at ‘http://webdavserver.com/myserver/’ and the item URL is ‘http://webdavserver.com/myserver/myfolder/myitem.docgetPath() method implementation must return ‘myfolder/myitem.doc’. To calculate the entire item URL the framework will call javax.servlet.http.HttpServletRequest.getContextPath() method and concatenate it with url returned by getPath() method.

Note that when you rename the item in a WebDAV client, the HierarchyItem.moveTo() method is called, there is no any separate method for renaming the item.

Folder Interface

Besides methods provided by HierarchyItem, the Folder interface provides a method for enumerating folder children as well as methods for creating files and folders. In your Folder.getChildren() implementation, you must create and return the list of files and folders contained in this folder. Often in your getChildren() implementation you will also verify user permissions and return only items that the user has rights to see.

File Interface

In addition to HierarchyItem methods, File interface has members specific for files: methods for getting MIME type, length of the file content in bytes, and methods for reading and writing resource content.

When WebDAV client is uploading a file to the server the File.write() method is called.  The IT Hit Java WebDAV Server Library supports upload of the file in segments - resumable upload. The WebDAV client application can submit to server either entire file or submit only a part of a file. The framework can process both cases and File.write() has parameters that provide information to implementers about position of the submitted segment inside content and total file size:

public class FileImpl implements File {

...

     public void write(InputStream content, String contentType, long startIndex, long totalFileSize)
            throws LockedException, ServerException, IOException {
        try {
            if (startIndex == 0) {
                getDataAccess().executeUpdate("UPDATE Repository SET"
                        + " Modified = ?,"
                        + " Content = EMPTY_BLOB(),"
                        + " ContentType = ?"
                        + " WHERE ID = ?", new java.sql.Date(new Date().getTime()), contentType, getId());

            } else {
                getDataAccess().executeUpdate("UPDATE Repository SET"
                        + " Modified = ?,"
                        + " ContentType = ?"
                        + " WHERE ID = ?", new java.sql.Date(new Date().getTime()), contentType, getId());


            }

            Blob bb = getDataAccess().executeScalar("select content from Repository where id = ? for update", getId());

            OutputStream os = bb.setBinaryStream(startIndex);
            try {
                int read;
                int bufSize = 1048576; // 1Mb
                byte[] buf = new byte[bufSize];
                while ((read = content.read(buf)) > 0) {
                    os.write(buf, 0, read);
                }
            }
            finally {
                os.close();
            }
        }
        catch (SQLException ex) {
            throw new ServerException(ex);
        }
    }

...

}

Note to WebDAV client implementers: To submit a file segment attach the Content-Range header to PUT request: Content-Range: bytes XXX-XXX/XXX. If no Content-Range header is found the IT Hit Java WebDAV Server Library assumes the entire content is submitted.

The IT Hit WebDAV Server Engine also supports file upload via multipart-encoded form using POST verb. Read here about how to upload files from a web page.

For getting file content, the File interface provides read() method. The WebDAV client application can request  either entire file or only a file segment. The IT Hit Java WebDAV Server Library can handle both cases, providing parameters in File.read() that specify which bytes of the file are being requested:

 public void read(OutputStream output, long startIndex, long count) throws ServerException {
        try {
            Blob blob = getDataAccess().executeScalar("SELECT Content FROM Repository WHERE ID = ?", id);
            if (blob != null) {
                InputStream stream = blob.getBinaryStream();
                try {
                    int bufSize = 1048576; // 1Mb
                    byte[] buf = new byte[bufSize];
                    long retval;
                    stream.skip(startIndex);
                    while ((retval = stream.read(buf)) > 0) {
                        try {
                            output.write(buf, 0, (int) retval);
                        }
                        catch (IOException e) {
                            getEngine().getLogger().logDebug("Remote host closed connection");
                            return;
                        }

                        startIndex += retval;
                        count -= retval;
                    }
                }
                finally {
                    stream.close();
                }
            }
        }
        catch (SQLException e) {
            throw new ServerException(e);
        } catch (IOException e) {
            throw new ServerException(e);
        }
    }

Usually, the segmented download is used by download managers to restore broken or paused downloads.

Determining File Content-Type / Mime-Type

Most WebDAV clients, including Web Folders, do not submit Content-Type header when uploading a file, so the contentType parameter of the File.write() method will be null in most cases. However providing the correct Content-Type / mime-type is vital when serving file content. Some browsers, such as Firefox, rely on the Mime-Type returned in Content-Type header to determine what action to take when the user clicks on file hyperlink. Depending on the mime-type provided by the server, the browser can either load file content in a browser window or can display File Open / File Save dialog.

To get the file mime-type the IT Hit Java WebDAV Server Library provides MimeType class that returns mime-type by extension. Usually you will use this class in File.getContentType() method implementation:

public String getContentType() throws ServerException {
        String contentType = getDataAccess().executeScalar("SELECT ContentType FROM Repository WHERE ID = ?", id);

        if (contentType == null || contentType.length() == 0) {
            String name = this.getName();
            int periodIndex = name.lastIndexOf('.');
            String ext = name.substring(periodIndex + 1, name.length());
            contentType = MimeType.getInstance().getMimeType(ext);
            if (contentType == null)
                contentType = "application/octet-stream";
        }
        return contentType;
    }

Note that unlike Firefox, Internet Explorer relies on file extension when deciding what action to take.

Custom Response Code

By default, the IT Hit Java WebDAV Server Library assumes that the request was processed successfully and returns response code specified in WebDAV or other standards. In case you need to return any custom response code, you can throw an instance of ServerException class.
Often you will throw the exception to inform the client that there is not enough permissions for accessing specific file or creating file or folder:

    
public void createFolder(String name) throws LockedException, ServerException {
        if(/* user does not have enogh permissions */)
            throw new ServerException(WebDavStatus.ACCESS_DENIED);
        ...
    }

Custom Properties

Each item in a repository can have custom string properties associated with the item. Some WebDAV client applications, for instance Microsoft Office and Microsoft Mini-redirector (Windows WebDAV client) can set and read custom properties.

Each property is represented by Property class and has Name, Namespace, and Value. For manipulating string properties HierarchyItem interface provides getProperties(), getPropertyNames() and updateProperties() methods. When a client application requests properties the framework calls getProperties() passing the list of properties requested by client or null if all properties are requested. In your implementation, you will create and return the list of properties.

To assist with retrieving properties the Folder.GetChildren() and Search.Search() methods provide a properties list parameter. Using this parameter you can extract all needed properties for all items from your back-end storage in one request and save in your HierarhyItem implementation object. After calling GetChildren() (or Search()) methods, the framework will call HierarchyItem.GetProperties() for each item, you will return saved properties to the Engine.

If any property failed to create, update or delete you can throw MultistatusException, to provide client with information which properties failed to update. You must add description and status for each property using MultistatusException.addResponse() overloaded methods. Note that some WebDAV clients, for example Microsoft Mini-redirector, will ignore error descriptions and will display a generic error.

To get the names of available custom properties you can implement getPropertyNames() method. However most WebDAV clients never request property names without values, so the implementation usually could be left blank.