There are a variety of ways you can enhance your application with WebDAV access which we will be going into in a latter part of the WebDAV series. For now we will show you how to create a basic online network drive using WebDAV via Milton, a Java WebDAV Server library.
Example
We will write the logic to create a network drive containing just one file, scratchpad.txt, which can be modified at will from any location where the drive is mounted.
You may want to load the project into your IDE so that you can follow along.
Step 1: Create The Domain Object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class Scratchpad { private static final Scratchpad INSTANCE = new Scratchpad(); private String text = ""; private Date modifiedDate; private final Date createdDate = new Date(); public static Scratchpad get() { return INSTANCE; } public String getText() { return this.text; } public void setText(String text) { this.text = text; this.modifiedDate = new Date(); } public Date getModifiedDate() { return modifiedDate; } public Date getCreatedDate() { return createdDate; } } |
This is a simple POJO which will allow us to hold some text and attributes which will nicely represent a virtual scratchpad.txt file. Your applications could have something similar which would benefit from being exposed though WebDAV.
Step 2: Create The Equivalent Resource Class As A Composition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
public class ScratchpadResource implements PropFindableResource, GetableResource { private static final String FILENAME = "scratchpad.txt"; public static String getFilename() { return FILENAME; } @Override public String getUniqueId() { return getFilename(); } @Override public String getName() { return getFilename(); } @Override public Object authenticate(String user, String password) { return "anonymous"; } @Override public boolean authorise(Request request, Method method, Auth auth) { return true; } @Override public String getRealm() { return null; } @Override public Date getCreateDate() { return Scratchpad.get().getCreatedDate(); } @Override public Date getModifiedDate() { return Scratchpad.get().getModifiedDate(); } @Override public String checkRedirect(Request request) { return null; } @Override public Long getMaxAgeSeconds(Auth auth) { return null; } @Override public String getContentType(String accepts) { return "text/plain"; } @Override public Long getContentLength() { return Long.valueOf(Scratchpad.get().getText().length()); } @Override public void sendContent(OutputStream out, Range range, Map params, String contentType) throws IOException, NotAuthorizedException, BadRequestException { out.write(Scratchpad.get().getText().getBytes()); } } |
In this step, we have created a class with a “has-a” relationship with the original domain object. The key part here is to note which interfaces are implemented. Implementing PropFindableResource allows the file or folder to be searchable. Implementing GetableResource allows you to define how WebDAV will get the content of the virtual file or folder. You should pick the interfaces based on the behavior of the resource. In this example, we want the file to be able to be read, and modified, but never deleted. Subsequently we have implemented only the required interfaces. For more details on these interfaces, please refer to the Milton JavaDoc.
The methods implementations are done in this way so that the virtual scratchpad.txt file behaves like you would expect a text file to behave. Notably sendContent(…) will get the text stored in the POJO and supply it to the OutputStream.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
public class RootResource implements PropFindableResource, CollectionResource, PutableResource { private static final String FOLDERNAME = "root"; @Override public String getUniqueId() { return FOLDERNAME; } @Override public String getName() { return FOLDERNAME; } @Override public Object authenticate(String user, String password) { return "anonymous"; } @Override public boolean authorise(Request request, Method method, Auth auth) { return true; } @Override public String getRealm() { return null; } @Override public Date getCreateDate() { return Scratchpad.get().getCreatedDate(); } @Override public Date getModifiedDate() { return Scratchpad.get().getCreatedDate(); } @Override public String checkRedirect(Request request) { return null; } @Override public Resource child(String childName) { return null; } @Override public List getChildren() { List resources = new ArrayList(); resources.add(new ScratchpadResource()); return resources; } @Override public Resource createNew(String newName, InputStream inputStream, Long length, String contentType) throws IOException, ConflictException, NotAuthorizedException, BadRequestException { if (ScratchpadResource.getFilename().equals(newName)) { StringWriter writer = new StringWriter(); IOUtils.copy(inputStream, writer, "UTF-8"); Scratchpad.get().setText(writer.toString()); return new ScratchpadResource(); } else { throw new BadRequestException(this); } } } |
To keep the file-structure analogy going, we should have a root folder to contain our one file. That’s the purpose of the RootResource class. Once again, key here is to determine which interfaces are implemented based on the behavior that we seek. Implementing PutableResource allows files to be created within this folder using the PUT request.
The methods implementations are done in this way so that the scratchpad.txt file can have it’s contents modified. Any other file that is attempted to be created within the root folder will return an error.
Step 3: Implement The ResourceFactory To Create Your Tree Structure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class MyResourceFactory implements ResourceFactory { @Override public Resource getResource(String host, String strPath) { Path path = Path.path(strPath); //STRIP PRECEEDING PATH path = path.getStripFirst().getStripFirst(); if (path.isRoot()) { return new RootResource(); } else if (path.getFirst().equals(ScratchpadResource.getFilename())) { return new ScratchpadResource(); } return null; } } |
Here is where you define how your tree structure will work. What you want to do is think about which resource is being referred to for a given URL. In our simple case, we have just two possible valid URLS: http:///scratchpad/ and http:///scratchpad/scratchpad.txt .
This makes defining our tree structure pretty simple to code up as you can see below. If your tree structure is more complex with several child folders, you may want to consider a recursive strategy.
Step 4: Implement The ResourceFactoryFactory To Allow Milton To Access Your Resource Classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class MyResourceFactoryFactory implements ResourceFactoryFactory { private static AuthenticationService authenticationService; private static MyResourceFactory resourceFactory; @Override public ResourceFactory createResourceFactory() { return resourceFactory; } @Override public WebDavResponseHandler createResponseHandler() { return new DefaultWebDavResponseHandler(authenticationService); } @Override public void init() { if (authenticationService == null) { authenticationService = new AuthenticationService(); resourceFactory = new MyResourceFactory(); } } } |
This class exists most importantly to associate the MiltonServlet with the ResourceFactory implementation in the previous step. You can use this class to load some initialization parameters but for our simple example, it was not necessary.
Step 5: Update Your web.xml To Allow Access To Your Milton Servlet And Classes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>webdav-example</display-name> ... <filter> <display-name>WebDAVFilter</display-name> <filter-name>WebDAVFilter</filter-name> <filter-class>com.northconcepts.webdav.WebDAVFilter</filter-class> </filter> <filter-mapping> <filter-name>WebDAVFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ... </web-app> |
Finally, to direct requests appropriately, you will need to update the web.xml so that the MiltonServlet can serve up the content for the desired URLs.
That’s it! You can now point your web browser, WebDAV client, or network drive mount to the server URL that is running this project and you will see the root folder containing the scratchpad.txt file. You can open and edit it as you wish and the content is persisted and automatically shared across any computer that has shared that drive.
Concluding Thoughts
Our example made a single POJO accessible from a cloud. What’s remarkable about WebDAV shows itself when you start to think about the business objects in your application that can be represented in a file format. This would allow direct, real-time access to read and update this data in a way that is both natural and simple to code.
Future WebDAV posts will explore this thought as well as touch on some of the more advanced capabilities WebDAV offers that you find in full-fledged Content Management Systems.
Download
The WebDAV download contains the entire source code (including Eclipse project). The source code is licensed under the terms of the Apache License, Version 2.0.
If you enjoyed this, We’ve got an exception tracking tool for Java coming out. Sign-up at StackHunter.com to be notified when it does.
The example code does not work out of the box, I get the following error:
2011-08-02 14:45:49,491 [http-8080-2] DEBUG com.bradmcevoy.http.http11.GetHandler – process
2011-08-02 14:45:49,496 [http-8080-2] INFO com.bradmcevoy.http.ResourceHandlerHelper – resource not compatible. Resource class: class com.northconcepts.webdav.RootResource handler: class com.bradmcevoy.http.http11.GetHandler
And the web page shows “Method Not Implemented”
Any ideas? Thanks for the article!
Hi Colin,
Did you try the URL http://localhost/webdav-example/WebDAV/ itself? I ask because the GetableResource interface isn’t implemented for the RootResource class, so that’s why you would get the “Method Not Implemented” for a HTTP GET on that URL.
Instead try the URL http://localhost/webdav-example/WebDAV/scratchpad.txt on your browser (correcting for your port number if not port 80) to see the response.
Note that I updated the example code as well to correct an unrelated encoding issue, so you may want to download the latest ZIP.
Yeah my own silly mistake, thanks for pointing me to the right URL to use 🙂
Pingback: Anyone using Milton webdav library? - Programmers Goodies
Hi,
im getting an exception altough i added all necesary jar files (or at least all i was aware):
java.lang.ClassNotFoundException: com.bradmcevoy.http.Response
What can cause this? What did i forget to add? Thx.
Pingback: How To Manage Your Application Properties using WebDAV | North Concepts
Hello.
Where are compiled files deployed? I checked tomcat/webapps folder, but there was no folder related to this project. Where shall I find the compiled project files after deployment and where to place files I want to share via DAV protocol?
Thank you.
Hi,
I deploy this project onto my host, go to a browser and input: http://my_host_name.com:8180/webdav-example/WebDAV/scratchpad.txt and all I got is a blank page. No complaint at all. According to your text:
” You can now point your web browser… to the server URL that is running this project and you will see the root folder containing the scratchpad.txt file”
I guess it’s NOT successful, is it? Am I missing something here?
Hi,
It works fine. But I have one more requirement. How can I ask the users for username and password when the user access it from Desktop client.
Thank you
Found the solution, used http://securityfilter.sourceforge.net/
Or just implement the authenticate and authorise methods. They’re stubbed out in this example:
@Override
public Object authenticate(String user, String password) {
return “anonymous”;
}
@Override
public boolean authorise(Request request, Method method, Auth auth) {
return true;
}
But implement logic to check credentials (authenticate) and the to check the user has access to the request (authorise)
Thanks for the great article! Really useful 🙂