Commits

Sam Adams  committed 34689e3

Developing client

  • Participants
  • Parent commits f03e5d1

Comments (0)

Files changed (29)

File client-cli/pom.xml

             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>net.chempound</groupId>
+            <artifactId>chempound-webapp</artifactId>
+            <version>0.1-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>

File client-cli/src/main/java/net/chempound/client/cli/ChempoundClientCLI.java

     private static final String CHEMPOUND = "chempound";
         
     private static final String C_DEPOSIT = "deposit";
+    private static final String C_CREATE_COLLECTION = "create-collection";
     private static final String C_COPY = "copy";
     private static final String C_DELETE = "delete";
     private static final String C_EXPORT = "export";
     }
 
     protected void installCommands(final JCommander jCommander) {
-        addCommand(jCommander, C_COPY, new CopyCommand());
-        addCommand(jCommander, C_DELETE, new DeleteCommand());
+//        addCommand(jCommander, C_COPY, new CopyCommand());
+        addCommand(jCommander, C_CREATE_COLLECTION, new CreateCollectionCommand());
+//        addCommand(jCommander, C_DELETE, new DeleteCommand());
         addCommand(jCommander, C_DEPOSIT, new DepositCommand());
-        addCommand(jCommander, C_EXPORT, new ExportCommand());
-        addCommand(jCommander, C_HELP, new HelpCommand());
-        addCommand(jCommander, C_MOVE, new MoveCommand());
+//        addCommand(jCommander, C_EXPORT, new ExportCommand());
+//        addCommand(jCommander, C_HELP, new HelpCommand());
+//        addCommand(jCommander, C_MOVE, new MoveCommand());
     }
 
     protected void addCommand(final JCommander jCommander, final String alias, Command<?> command) {
     protected void basicUsage() {
         System.out.println("Chempound Client CLI");
         System.out.println();
-        System.out.println("basic commands:");
-        System.out.println();
+//        System.out.println("basic commands:");
+//        System.out.println();
 
         final int maxLen = getMaxCommandLength();
         for (final String command : jCommander.getCommands().keySet()) {
         }
 
         System.out.println();
-        System.out.println("use \"chempound help\" for the full list of commands");
+//        System.out.println("use \"chempound help\" for the full list of commands");
     }
 
     private int getMaxCommandLength() {

File client-cli/src/main/java/net/chempound/client/cli/command/CreateCollectionCommand.java

+package net.chempound.client.cli.command;
+
+import com.hp.hpl.jena.vocabulary.RDF;
+import net.chempound.client.SwordClient;
+import net.chempound.client.cli.options.CreateCollectionOptions;
+import net.chempound.client.cli.options.GlobalOptions;
+import net.chempound.content.DefaultDepositRequest;
+import net.chempound.content.DepositRequest;
+import net.chempound.rdf.CPTerms;
+
+import java.net.URI;
+
+/**
+ * @author Sam Adams
+ */
+public class CreateCollectionCommand implements Command<CreateCollectionOptions> {
+
+    private final CreateCollectionOptions options = new CreateCollectionOptions();
+
+    @Override
+    public void invoke(final GlobalOptions globalOptions) {
+        final SwordClient swordClient = new SwordClient();
+        final DepositRequest depositRequest = new DefaultDepositRequest();
+        if (options.args.isEmpty()) {
+            throw new RuntimeException("Title must be specified");
+        }
+        final String title = options.args.get(0);
+        depositRequest.setTitle(title);
+        if (options.slug == null) {
+            depositRequest.setSlug(title);
+        } else {
+            depositRequest.setSlug(options.slug);
+        }
+        depositRequest.getMetadataModel().getResource("").addProperty(RDF.type, CPTerms.Collection);
+
+        final URI uri = URI.create(globalOptions.repository);
+
+        try {
+            swordClient.deposit(uri, depositRequest);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public CreateCollectionOptions getOptions() {
+        return options;
+    }
+}

File client-cli/src/main/java/net/chempound/client/cli/command/DepositCommand.java

 import net.chempound.client.cli.options.GlobalOptions;
 import net.chempound.storage.LocalFileResource;
 import net.chempound.storage.LocalResource;
+import org.apache.commons.io.FilenameUtils;
 import org.swordapp.client.Deposit;
 import org.swordapp.client.DepositReceipt;
 import org.swordapp.client.SWORDClient;
 
         final DepositBuilder depositBuilder = new DepositBuilder();
 
-        if (depositOptions.slug != null) {
-            depositBuilder.setSlug(depositOptions.slug);
-        }
-
-        if (depositOptions.title != null) {
-            depositBuilder.setTitle(depositOptions.title);
-        }
-
-        if (depositOptions.files != null) {
-            for (final String filename : depositOptions.files) {
-                final File file = new File(filename);
-                final LocalResource resource = new LocalFileResource(filename, file);
-                depositBuilder.addResource(resource);
-            }
-        }
+        attachFiles(globalOptions, depositBuilder);
+        setTitle(depositBuilder);
+        setSlug(depositBuilder);
 
         final SWORDClient swordClient = new SWORDClient();
         final String url = globalOptions.repository;
 
     }
 
+    private void setTitle(final DepositBuilder depositBuilder) {
+        if (depositOptions.title != null) {
+            depositBuilder.setTitle(depositOptions.title);
+        }
+    }
+
+    private void setSlug(final DepositBuilder depositBuilder) {
+        if (depositOptions.slug != null) {
+            depositBuilder.setSlug(depositOptions.slug);
+        }
+    }
+
+    private void attachFiles(final GlobalOptions globalOptions, final DepositBuilder depositBuilder) {
+        if (depositOptions.files != null) {
+            for (final String filename : depositOptions.files) {
+                final File file = globalOptions.workingDirectory == null ? new File(filename) : new File(globalOptions.workingDirectory, filename);
+                final String depositFilename = FilenameUtils.getName(filename);
+                final LocalResource resource = new LocalFileResource(depositFilename, file);
+                depositBuilder.addResource(resource);
+            }
+        }
+    }
+
 }

File client-cli/src/main/java/net/chempound/client/cli/options/CreateCollectionOptions.java

+package net.chempound.client.cli.options;
+
+import com.beust.jcommander.Parameter;
+
+import java.util.List;
+
+/**
+ * @author Sam Adams
+ */
+public class CreateCollectionOptions {
+
+    @Parameter(names = {"-s", "--slug"})
+    public String slug;
+
+    @Parameter(names = {"-d", "--description"})
+    public String description;
+
+    @Parameter(names = {"-a", "--accepts"})
+    public List<String> accepts;
+
+    @Parameter(description = "The title", arity = 1)
+    public List<String> args;
+
+}

File client-cli/src/main/java/net/chempound/client/cli/options/GlobalOptions.java

 
 import com.beust.jcommander.Parameter;
 
+import java.io.File;
+
 public class GlobalOptions {
 
     @Parameter(names = {"-q", "--quiet"}, description = "enable additional output")
     @Parameter(names = {"-R", "--repository"}, description = "target repository")
     public String repository;
 
+    @Parameter(names = {"--cwd"}, description = "change working directory")
+    public File workingDirectory;
+
 }

File client-cli/src/main/java/net/chempound/client/cli/options/deposit/CifDepositOptions.java

-package net.chempound.client.cli.options.deposit;
-
-import net.chempound.client.cli.options.GlobalOptions;
-
-/**
- * @author Sam Adams
- */
-public class CifDepositOptions extends GlobalOptions {
-}

File client-cli/src/main/java/net/chempound/client/cli/options/deposit/GaussianDepositOptions.java

-package net.chempound.client.cli.options.deposit;
-
-/**
- * @author Sam Adams
- */
-public class GaussianDepositOptions {
-}

File client-cli/src/main/java/net/chempound/client/cli/options/deposit/NwChemDepositOptions.java

-package net.chempound.client.cli.options.deposit;
-
-/**
- * @author Sam Adams
- */
-public class NwChemDepositOptions {
-}

File client-cli/src/test/java/net/chempound/client/cli/ChempoundClientCLIIntegrationTest.java

+package net.chempound.client.cli;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Stage;
+import net.chempound.ChempoundConfigurationModule;
+import net.chempound.DefaultChempoundModule;
+import net.chempound.config.ChempoundConfiguration;
+import net.chempound.config.DefaultChempoundConfiguration;
+import net.chempound.datastore.TripleStore;
+import net.chempound.rdf.ORE;
+import net.chempound.webapp.DefaultChempoundWebModule;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.junit.*;
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.data.Protocol;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.UUID;
+
+import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral;
+import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource;
+import static com.hp.hpl.jena.vocabulary.RDF.type;
+import static net.chempound.rdf.CPTerms.Collection;
+import static net.chempound.rdf.CPTerms.Item;
+import static net.chempound.rdf.DCTerms.title;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.spy;
+
+/**
+ * @author Sam Adams
+ */
+public class ChempoundClientCLIIntegrationTest {
+
+    private static Component component;
+    private static TripleStore tripleStore;
+    private static File workspace;
+
+    private ChempoundClientCLI client;
+    private File tmpDir;
+
+    @BeforeClass
+    public static void setUpChempound() throws Exception {
+        final URI baseUri = URI.create("http://localhost:12080/");
+        workspace = new File("target", UUID.randomUUID().toString());
+
+        final ChempoundConfiguration configuration = new DefaultChempoundConfiguration(baseUri, workspace);
+        final Injector injector = Guice.createInjector(Stage.DEVELOPMENT,
+                new DefaultChempoundModule(),
+                new DefaultChempoundWebModule(),
+                new ChempoundConfigurationModule(configuration)
+
+        );
+
+        component = injector.getInstance(Component.class);
+        component.getServers().add(Protocol.HTTP, 12080)
+                .getContext().getParameters().add("maxThreads", "256");
+        component.getDefaultHost().attach(injector.getInstance(Application.class));
+        component.start();
+
+        tripleStore = injector.getInstance(TripleStore.class);
+    }
+
+    @AfterClass
+    public static void tearDownChempound() throws Exception {
+        if (component != null) {
+            try {
+                component.stop();
+            } finally {
+                component = null;
+            }
+        }
+        FileUtils.deleteQuietly(workspace);
+        workspace = null;
+        tripleStore = null;
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        client = spy(new ChempoundClientCLI());
+        doNothing().when(client).exit(anyInt());
+
+        tmpDir = new File("target/" + UUID.randomUUID());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (tmpDir != null) {
+            FileUtils.deleteQuietly(tmpDir);
+        }
+    }
+
+    @Test
+    public void testCreateCollection() throws Exception {
+        client.runMain("-R", "http://localhost:12080/sword/collection/",
+                "create-collection", "FooBar");
+
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/FooBar/"), type, Collection));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/FooBar/"), title, createPlainLiteral("FooBar")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/"), ORE.aggregates, createResource("http://localhost:12080/content/FooBar/")));
+    }
+
+    @Test
+    public void testCreateCollectionWithTitle() throws Exception {
+        client.runMain("-R", "http://localhost:12080/sword/collection/",
+                "create-collection", "--slug", "foobar", "FooBar");
+
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/foobar/"), type, Collection));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/foobar/"), title, createPlainLiteral("FooBar")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/"), ORE.aggregates, createResource("http://localhost:12080/content/foobar/")));
+    }
+
+    @Test
+    public void testDepositItem() throws Exception {
+        FileUtils.forceMkdir(tmpDir);
+        FileUtils.writeStringToFile(new File(tmpDir, "wibble.txt"), "wibble");
+
+        client.runMain("-R", "http://localhost:12080/sword/collection/",
+                "deposit", "wibble.txt");
+
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/foobar/"), type, Collection));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/foobar/"), title, createPlainLiteral("FooBar")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/"), ORE.aggregates, createResource("http://localhost:12080/content/foobar/")));
+    }
+
+    @Test
+    public void testDepositItemWithSlugAndTitle() throws Exception {
+        FileUtils.forceMkdir(tmpDir);
+        FileUtils.writeStringToFile(new File(tmpDir, "wibble.txt"), "wibble");
+
+        client.runMain("-R", "http://localhost:12080/sword/collection/", "--cwd", tmpDir.getAbsolutePath(),
+                "deposit", "-s", "flob", "-t", "##title##", "wibble.txt");
+
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/flob/"), type, Item));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/flob/"), title, createPlainLiteral("##title##")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/flob/"), ORE.aggregates, createResource("http://localhost:12080/content/flob/wibble.txt")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/"), ORE.aggregates, createResource("http://localhost:12080/content/flob/")));
+
+        assertEquals("wibble", fetchUrl(URI.create("http://localhost:12080/content/flob/wibble.txt")));
+    }
+
+    @Test
+    public void testDepositMultipleItems() throws Exception {
+        FileUtils.forceMkdir(tmpDir);
+        FileUtils.writeStringToFile(new File(tmpDir, "wibble.txt"), "wibble");
+        FileUtils.writeStringToFile(new File(tmpDir, "wobble.txt"), "wobble");
+
+        client.runMain("-R", "http://localhost:12080/sword/collection/", "--cwd", tmpDir.getAbsolutePath(),
+                "deposit", "-s", "test", "wibble.txt", "wobble.txt");
+
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/test/"), ORE.aggregates, createResource("http://localhost:12080/content/test/wibble.txt")));
+        assertTrue(tripleStore.containsTriple(createResource("http://localhost:12080/content/test/"), ORE.aggregates, createResource("http://localhost:12080/content/test/wobble.txt")));
+
+        assertEquals("wibble", fetchUrl(URI.create("http://localhost:12080/content/test/wibble.txt")));
+        assertEquals("wobble", fetchUrl(URI.create("http://localhost:12080/content/test/wobble.txt")));
+    }
+
+    private static String fetchUrl(final URI uri) throws IOException {
+        InputStream in = uri.toURL().openStream();
+        try {
+            return IOUtils.toString(in);
+        } finally {
+            in.close();
+        }
+    }
+
+}

File client-cli/src/test/java/net/chempound/client/cli/ChempoundClientCLITest.java

 package net.chempound.client.cli;
 
 import net.chempound.client.cli.command.Command;
+import net.chempound.client.cli.command.CreateCollectionCommand;
 import net.chempound.client.cli.command.DepositCommand;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
-import java.util.Arrays;
-
+import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Mockito.*;
 
         verify(chempoundClient).invokeCommand(any(DepositCommand.class));
         DepositCommand depositCommand = (DepositCommand) command;
-        assertEquals(Arrays.asList("file1.foo", "file2.bar"), depositCommand.getOptions().files);
+        assertEquals(asList("file1.foo", "file2.bar"), depositCommand.getOptions().files);
     }
 
     @Test
     public void testDepositWithTitleAndSlug() {
-        chempoundClient.runMain("deposit", "-t", "--title--", "-s", "--slug--", "file1.foo", "file2.bar");
+        chempoundClient.runMain("deposit", "-t", "##title##", "-s", "##slug##", "file1.foo", "file2.bar");
         verify(chempoundClient).invokeCommand(any(DepositCommand.class));
         DepositCommand depositCommand = (DepositCommand) command;
-        assertEquals("--title--", depositCommand.getOptions().title);
-        assertEquals("--slug--", depositCommand.getOptions().slug);
-        assertEquals(Arrays.asList("file1.foo", "file2.bar"), depositCommand.getOptions().files);
+        assertEquals("##title##", depositCommand.getOptions().title);
+        assertEquals("##slug##", depositCommand.getOptions().slug);
+        assertEquals(asList("file1.foo", "file2.bar"), depositCommand.getOptions().files);
+    }
+
+    @Test
+    public void testCreateCollection() {
+        chempoundClient.runMain("create-collection", "##title##");
+
+        verify(chempoundClient).invokeCommand(any(CreateCollectionCommand.class));
+        CreateCollectionCommand createCollection = (CreateCollectionCommand) command;
+        assertEquals(asList("##title##"), createCollection.getOptions().args);
+    }
+
+    @Test
+    public void testCreateCollectionWithTitle() {
+        chempoundClient.runMain("create-collection", "-s", "##id##", "##title##");
+
+        verify(chempoundClient).invokeCommand(any(CreateCollectionCommand.class));
+        CreateCollectionCommand createCollection = (CreateCollectionCommand) command;
+        assertEquals("##id##", createCollection.getOptions().slug);
+        assertEquals(asList("##title##"), createCollection.getOptions().args);
     }
 
 }

File client-cli/src/test/java/net/chempound/client/cli/command/CreateCollectionCommandTest.java

+package net.chempound.client.cli.command;
+
+/**
+ * @author Sam Adams
+ */
+public class CreateCollectionCommandTest {
+}

File client-cli/src/test/java/net/chempound/client/cli/command/DepositCommandTest.java

+package net.chempound.client.cli.command;
+
+/**
+ * @author Sam Adams
+ */
+public class DepositCommandTest {
+}

File client-utils/pom.xml

             <artifactId>chempound-api</artifactId>
             <version>0.1-SNAPSHOT</version>
         </dependency>
+
+        <dependency>
+            <groupId>net.sf.atomxom</groupId>
+            <artifactId>atomxom-core</artifactId>
+            <version>${atomxom.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.swordapp</groupId>
             <artifactId>sword-client</artifactId>
         </dependency>
 
         <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpmime</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ccil.cowan.tagsoup</groupId>
+            <artifactId>tagsoup</artifactId>
+            <version>1.2.1</version>
+        </dependency>
+
+
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-log4j12</artifactId>
             <version>[1.6.1]</version>

File client-utils/src/main/java/net/chempound/client/AttachmentBodyPart.java

+package net.chempound.client;
+
+import org.apache.http.entity.mime.MIME;
+import org.apache.http.entity.mime.content.ContentBody;
+import org.apache.james.mime4j.descriptor.ContentDescriptor;
+import org.apache.james.mime4j.message.BodyPart;
+import org.apache.james.mime4j.message.Header;
+import org.apache.james.mime4j.parser.Field;
+import org.apache.james.mime4j.util.ByteSequence;
+import org.apache.james.mime4j.util.ContentUtil;
+
+/**
+ * @author Sam Adams
+ */
+public class AttachmentBodyPart extends BodyPart {
+
+    private final String name;
+    private final String md5;
+
+    public AttachmentBodyPart(final String name, final ContentBody body, final String md5) {
+        super();
+        if (name == null) {
+            throw new IllegalArgumentException("Name may not be null");
+        }
+        if (body == null) {
+            throw new IllegalArgumentException("Body may not be null");
+        }
+        this.name = name;
+        this.md5 = md5;
+
+        Header header = new Header();
+        setHeader(header);
+        setBody(body);
+
+        generateContentDisp(body);
+        generateContentType(body);
+        generateTransferEncoding(body);
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    protected void generateContentDisp(final ContentBody body) {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append("attachment; name=\"");
+        buffer.append(getName());
+        buffer.append("\"");
+        if (body.getFilename() != null) {
+            buffer.append("; filename=\"");
+            buffer.append(body.getFilename());
+            buffer.append("\"");
+        }
+        addField(MIME.CONTENT_DISPOSITION, buffer.toString());
+        if (md5 != null) {
+            addField("Content-MD5", md5);
+        }
+    }
+
+    protected void generateContentType(final ContentDescriptor desc) {
+        if (desc.getMimeType() != null) {
+            StringBuilder buffer = new StringBuilder();
+            buffer.append(desc.getMimeType());
+            if (desc.getCharset() != null) {
+                buffer.append("; charset=");
+                buffer.append(desc.getCharset());
+            }
+            addField(MIME.CONTENT_TYPE, buffer.toString());
+        }
+    }
+
+    protected void generateTransferEncoding(final ContentDescriptor desc) {
+        if (desc.getTransferEncoding() != null) {
+            addField(MIME.CONTENT_TRANSFER_ENC, desc.getTransferEncoding());
+        }
+    }
+
+    private void addField(final String name, final String value) {
+        getHeader().addField(new MinimalField(name, value));
+    }
+
+
+    static class MinimalField implements Field {
+
+        private final String name;
+        private final String value;
+
+        private ByteSequence raw; // cache, recreated on demand
+
+        MinimalField(final String name, final String value) {
+            super();
+            this.name = name;
+            this.value = value;
+            this.raw = null;
+        }
+
+        public String getName() {
+            return this.name;
+        }
+
+        public String getBody() {
+            return this.value;
+        }
+
+        public ByteSequence getRaw() {
+            if (this.raw == null) {
+                this.raw = ContentUtil.encode(toString());
+            }
+            return this.raw;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buffer = new StringBuilder();
+            buffer.append(this.name);
+            buffer.append(": ");
+            buffer.append(this.value);
+            return buffer.toString();
+        }
+
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/ByteArrayBody.java

+package net.chempound.client;
+
+import org.apache.http.entity.mime.MIME;
+import org.apache.http.entity.mime.content.AbstractContentBody;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * @author Sam Adams
+ */
+public class ByteArrayBody extends AbstractContentBody {
+
+    private static final String NULL_CHARSET = null;
+
+    private final byte[] bytes;
+    private final String filename;
+    private final String charset;
+
+    public ByteArrayBody(final byte[] bytes, final String mimeType, final String filename, final String charset) {
+        super(mimeType);
+        this.charset = charset;
+        if (bytes == null) {
+            throw new IllegalArgumentException("Byte array may not be null");
+        }
+        this.bytes = new byte[bytes.length];
+        System.arraycopy(bytes, 0, this.bytes, 0, bytes.length);
+        this.filename = filename;
+    }
+
+    public ByteArrayBody(final byte[] bytes, final String mimeType, final String filename) {
+        this(bytes, mimeType, filename, NULL_CHARSET);
+    }
+
+    @Override
+    public void writeTo(final OutputStream out) throws IOException {
+        if (out == null) {
+            throw new IllegalArgumentException("Output stream may not be null");
+        }
+        out.write(bytes);
+        out.flush();
+    }
+
+    @Override
+    public String getTransferEncoding() {
+        return MIME.ENC_BINARY;
+    }
+
+    @Override
+    public String getCharset() {
+        return charset;
+    }
+
+    @Override
+    public long getContentLength() {
+        return bytes.length;
+    }
+
+    @Override
+    public String getFilename() {
+        return this.filename;
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/MultipartRelatedEntity.java

+package net.chempound.client;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.entity.mime.FormBodyPart;
+import org.apache.http.entity.mime.HttpMultipart;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.content.ContentBody;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.protocol.HTTP;
+import org.apache.james.mime4j.field.Fields;
+import org.apache.james.mime4j.message.Message;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * @author Sam Adams
+ */
+public class MultipartRelatedEntity implements HttpEntity {
+
+    /**
+     * The pool of ASCII chars to be used for generating a multipart boundary.
+     */
+    private final static char[] MULTIPART_CHARS =
+        "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+            .toCharArray();
+
+    private final Message message;
+    private final HttpMultipart multipart;
+    private final Header contentType;
+
+    private long length;
+    private volatile boolean dirty; // used to decide whether to recalculate length
+
+    public MultipartRelatedEntity(
+            HttpMultipartMode mode,
+            final String boundary,
+            final Charset charset) {
+        super();
+        this.multipart = new HttpMultipart("related");
+        this.contentType = new BasicHeader(
+                HTTP.CONTENT_TYPE,
+                generateContentType(boundary, charset));
+        this.dirty = true;
+
+        this.message = new Message();
+        org.apache.james.mime4j.message.Header header =
+          new org.apache.james.mime4j.message.Header();
+        this.message.setHeader(header);
+        this.multipart.setParent(message);
+        if (mode == null) {
+            mode = HttpMultipartMode.STRICT;
+        }
+        this.multipart.setMode(mode);
+        this.message.getHeader().addField(Fields.contentType(this.contentType.getValue()));
+    }
+
+    public MultipartRelatedEntity(final HttpMultipartMode mode) {
+        this(mode, null, null);
+    }
+
+    public MultipartRelatedEntity() {
+        this(HttpMultipartMode.STRICT, null, null);
+    }
+
+    protected String generateContentType(
+            final String boundary,
+            final Charset charset) {
+        StringBuilder buffer = new StringBuilder();
+        buffer.append("multipart/related; boundary=");
+        if (boundary != null) {
+            buffer.append(boundary);
+        } else {
+            Random rand = new Random();
+            int count = rand.nextInt(11) + 30; // a random size from 30 to 40
+            for (int i = 0; i < count; i++) {
+                buffer.append(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
+            }
+        }
+        if (charset != null) {
+            buffer.append("; charset=");
+            buffer.append(charset.name());
+        }
+        return buffer.toString();
+    }
+
+    public void addPart(final String name, final ContentBody contentBody) {
+        this.multipart.addBodyPart(new AttachmentBodyPart(name, contentBody, null));
+        this.dirty = true;
+    }
+
+    public boolean isRepeatable() {
+        List<?> parts = this.multipart.getBodyParts();
+        for (Iterator<?> it = parts.iterator(); it.hasNext(); ) {
+            FormBodyPart part = (FormBodyPart) it.next();
+            ContentBody body = (ContentBody) part.getBody();
+            if (body.getContentLength() < 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public boolean isChunked() {
+        return !isRepeatable();
+    }
+
+    public boolean isStreaming() {
+        return !isRepeatable();
+    }
+
+    public long getContentLength() {
+        if (this.dirty) {
+            this.length = this.multipart.getTotalLength();
+            this.dirty = false;
+        }
+        return this.length;
+    }
+
+    public Header getContentType() {
+        return this.contentType;
+    }
+
+    public Header getContentEncoding() {
+        return null;
+    }
+
+    public void consumeContent()
+        throws IOException, UnsupportedOperationException{
+        if (isStreaming()) {
+            throw new UnsupportedOperationException(
+                    "Streaming entity does not implement #consumeContent()");
+        }
+    }
+
+    public InputStream getContent() throws IOException, UnsupportedOperationException {
+        throw new UnsupportedOperationException(
+                    "Multipart related entity does not implement #getContent()");
+    }
+
+    public void writeTo(final OutputStream outstream) throws IOException {
+        this.multipart.writeTo(outstream);
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/SilentErrorHandler.java

+package net.chempound.client;
+
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+/**
+ * @author Sam Adams
+ */
+public class SilentErrorHandler implements ErrorHandler {
+
+    @Override
+    public void warning(final SAXParseException exception) throws SAXException {
+    }
+
+    @Override
+    public void error(final SAXParseException exception) throws SAXException {
+    }
+
+    @Override
+    public void fatalError(final SAXParseException exception) throws SAXException {
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/SwordClient.java

+package net.chempound.client;
+
+import com.hp.hpl.jena.rdf.model.Model;
+import net.chempound.client.sword.DepositReceipt;
+import net.chempound.client.sword.SwordEntry;
+import net.chempound.client.sword.SwordService;
+import net.chempound.content.DepositRequest;
+import net.chempound.rdf.RdfIO;
+import net.chempound.storage.DepositResource;
+import net.chempound.storage.LocalResource;
+import net.sf.atomxom.AtomBuilder;
+import nu.xom.*;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.content.ContentBody;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static net.chempound.client.atom.AtomPub.ATOMPUB_NAMESPACE;
+import static org.apache.commons.httpclient.HttpStatus.SC_CREATED;
+import static org.apache.commons.httpclient.HttpStatus.SC_OK;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordClient {
+
+    private static final XPathContext HTML_CONTEXT = new XPathContext("h", "http://www.w3.org/1999/xhtml");
+
+    private static final String SLUG = "Slug";
+    private static final String PACKAGING = "Packaging";
+    private static final String IN_PROGRESS = "In-Progress";
+    private static final String ON_BEHALF_OF = "On-Behalf-Of";
+    private static final String METADATA_RELEVANT = "Metadata-Relevant";
+
+    private static final String PACKAGING_SIMPLE_ZIP = "http://purl.org/net/sword/package/SimpleZip";
+
+    private final HttpClient httpClient;
+
+    public SwordClient() {
+        this(new DefaultHttpClient());
+    }
+
+    protected SwordClient(final HttpClient httpClient) {
+        this.httpClient = httpClient;
+    }
+
+    public URI findServiceDocumentUrl(final URI url) throws Exception {
+        final HttpResponse response = httpClient.execute(new HttpGet(url));
+        try {
+            if (SC_OK == response.getStatusLine().getStatusCode()) {
+                final Document doc = readHtml(response);
+                return findServiceDocumentUrl(doc);
+            } else {
+                throw new RuntimeException("Error loading repository page: "+response.getStatusLine());
+            }
+        } finally {
+            closeQuietly(response);
+        }
+    }
+
+    public SwordService getServiceDocument(final URI url) throws Exception {
+        return getServiceDocument(url, true);
+    }
+
+    public SwordService getServiceDocument(final URI url, final boolean discover) throws Exception {
+        final HttpResponse response = httpClient.execute(new HttpGet(url));
+        try {
+            if (SC_OK == response.getStatusLine().getStatusCode()) {
+                return getServiceDocument(discover, response);
+            } else {
+                throw new RuntimeException("Error loading repository page: "+response.getStatusLine());
+            }
+        } finally {
+            closeQuietly(response);
+        }
+    }
+
+    public DepositReceipt deposit(final URI uri, final DepositRequest deposit) throws IOException {
+        final HttpPost request = createDepositRequest(uri, deposit);
+
+        final HttpResponse response = httpClient.execute(request);
+        try {
+            if (response.getStatusLine().getStatusCode() == SC_CREATED) {
+                final Document doc = readSwordDocument(response);
+                return new DepositReceipt(doc);
+            } else {
+                throw new IOException(response.getStatusLine().toString());
+            }
+        } finally {
+            if (response.getEntity() != null) {
+                response.getEntity().consumeContent();
+            }
+        }
+    }
+
+    private HttpPost createDepositRequest(final URI uri, final DepositRequest deposit) throws IOException {
+        final HttpPost request = new HttpPost(uri);
+        if (deposit.getSlug() != null) {
+            request.setHeader(SLUG, deposit.getSlug());
+        }
+        if (deposit.isInProgress()) {
+            request.setHeader(IN_PROGRESS, "true");
+        }
+        if (deposit.getOnBehalfOf() != null) {
+            request.setHeader(ON_BEHALF_OF, deposit.getOnBehalfOf());
+        }
+        setDepositEntity(request, deposit);
+        return request;
+    }
+
+    private void setDepositEntity(final HttpEntityEnclosingRequest request, final DepositRequest deposit) throws IOException {
+        if (deposit.getAggregatedResources().isEmpty()) {
+            createMetadataDeposit(request, deposit);
+        } else {
+            createMultipartDeposit(request, deposit);
+        }
+    }
+
+    private void createMetadataDeposit(final HttpEntityEnclosingRequest request, final DepositRequest deposit) throws IOException {
+        final SwordEntry entry = createSwordEntry(deposit);
+
+        final ByteArrayEntity entity = new ByteArrayEntity(writeElement(entry, "UTF-8"));
+        entity.setContentType("application/atom+xml");
+
+        request.setEntity(entity);
+    }
+
+    private void createMultipartDeposit(final HttpEntityEnclosingRequest request, final DepositRequest deposit) throws IOException {
+        final SwordEntry entry = createSwordEntry(deposit);
+
+        final MultipartRelatedEntity entity = new MultipartRelatedEntity(HttpMultipartMode.STRICT);
+        entity.addPart("atom", createContentBody(entry));
+        entity.addPart("payload", createPayloadBody(deposit));
+
+        request.setHeader(PACKAGING, PACKAGING_SIMPLE_ZIP);
+        request.setEntity(entity);
+    }
+
+    private ContentBody createContentBody(final SwordEntry entry) throws IOException {
+        final byte[] bytes = writeElement(entry, "UTF-8");
+        return new ByteArrayBody(bytes, "application/atom+xml", "deposit.atom", "UTF-8");
+    }
+
+    private ContentBody createPayloadBody(final DepositRequest deposit) throws IOException {
+        return new ByteArrayBody(createZipFile(deposit), "application/zip", "deposit.zip");
+    }
+
+    private static byte[] createZipFile(final DepositRequest deposit) throws IOException {
+        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        final ZipOutputStream out = new ZipOutputStream(buffer);
+        int n = 0;
+        for (final DepositResource resource : deposit.getAggregatedResources()) {
+            if (resource instanceof LocalResource) {
+                final ZipEntry zipEntry = new ZipEntry(resource.getUri().toString());
+                out.putNextEntry(zipEntry);
+                final InputStream in = resource.openInputStream();
+                IOUtils.copy(in, out);
+                IOUtils.closeQuietly(in);
+                n++;
+            }
+        }
+        out.close();
+        return n == 0 ? null : buffer.toByteArray();
+    }
+
+    private byte[] writeElement(final Element element, final String charset) throws IOException {
+        return writeDocument(element.getDocument() == null ? new Document(element) : element.getDocument());
+    }
+
+    private byte[] writeDocument(final Document document) throws IOException {
+        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        final Serializer ser = new Serializer(buffer);
+        ser.write(document);
+        return buffer.toByteArray();
+    }
+
+    private SwordEntry createSwordEntry(final DepositRequest deposit) {
+        final SwordEntry entry = new SwordEntry();
+        entry.setTitle(deposit.getTitle());
+
+        final Element rdf = rdfToXom(deposit.getMetadataModel());
+        entry.appendChild(rdf);
+
+        return entry;
+    }
+
+    private static Element rdfToXom(final Model model) {
+        final byte[] bytes = RdfIO.writeRdf(model);
+        try {
+            final Element rdf = new Builder().build(new ByteArrayInputStream(bytes)).getRootElement();
+            return (Element) rdf.copy();
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private SwordService getServiceDocument(final boolean discover, final HttpResponse response) throws Exception {
+        final Document doc = readHtml(response);
+        if (isServiceDocument(doc)) {
+            return new SwordService(doc.getRootElement());
+        } else if (discover) {
+            final URI serviceDocumentUrl = findServiceDocumentUrl(doc);
+            if (serviceDocumentUrl != null) {
+                return getServiceDocument(serviceDocumentUrl, false);
+            }
+        }
+        return null;
+    }
+
+    private boolean isServiceDocument(final Document doc) {
+        final Element root = doc.getRootElement();
+        return ATOMPUB_NAMESPACE.equals(root.getNamespaceURI()) && "service".equals(root.getLocalName());
+    }
+
+    private URI findServiceDocumentUrl(final Document doc) {
+        final Nodes nodes = doc.query("//h:link[@rel='http://purl.org/net/sword/terms/service-document' or @rel='sword']/@href", HTML_CONTEXT);
+        return nodes.size() == 0 ? null : URI.create(nodes.get(0).getValue());
+    }
+
+    private Document readHtml(final HttpResponse response) throws SAXException, ParsingException, IOException {
+        final byte[] bytes = IOUtils.toByteArray(response.getEntity().getContent());
+        try {
+            final XMLReader parser = XMLReaderFactory.createXMLReader();
+            parser.setErrorHandler(new SilentErrorHandler());
+            return new Builder(parser).build(new ByteArrayInputStream(bytes));
+        } catch (Exception e) {
+            final XMLReader parser = XMLReaderFactory.createXMLReader("org.ccil.cowan.tagsoup.Parser");
+            return new Builder(parser).build(new ByteArrayInputStream(bytes));
+        }
+    }
+
+    private Document readSwordDocument(final HttpResponse response) throws IOException {
+        try {
+            final byte[] bytes = IOUtils.toByteArray(response.getEntity().getContent());
+            final XMLReader parser = XMLReaderFactory.createXMLReader();
+            parser.setErrorHandler(new SilentErrorHandler());
+            return new AtomBuilder(parser).build(new ByteArrayInputStream(bytes));
+        } catch (ParsingException e) {
+            throw new IOException(e);
+        } catch (SAXException e) {
+            throw new IOException(e);
+        }
+    }
+
+    private static void closeQuietly(final HttpResponse response) {
+        if (response.getEntity() != null) {
+            try {
+                response.getEntity().consumeContent();
+            } catch (IOException e) {
+            }
+        }
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/SwordUtil.java

 import com.hp.hpl.jena.rdf.model.Statement;
 import com.hp.hpl.jena.vocabulary.DC;
 import com.hp.hpl.jena.vocabulary.RDFS;
-import com.hp.hpl.jena.xmloutput.RDFXMLWriterI;
-import com.hp.hpl.jena.xmloutput.impl.Abbreviated;
 import net.chempound.content.DepositRequest;
 import net.chempound.rdf.DCTerms;
 import net.chempound.storage.DepositResource;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
+import static net.chempound.rdf.RdfIO.writeRdf;
+
 /**
  * @author Sam Adams
  */
     }
 
     private static void attachMetaData(final DepositRequest request, final EntryPart entryPart) {
-        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
-
-        final RDFXMLWriterI w = new Abbreviated();
-        w.setProperty("allowBadURIs", true);
-        w.write(request.getMetadataModel(), buffer, null);
-
         final Abdera abdera = new Abdera();
-        final Document doc = abdera.getParser().parse(new ByteArrayInputStream(buffer.toByteArray()));
+        final Document doc = abdera.getParser().parse(new ByteArrayInputStream(writeRdf(request.getMetadataModel())));
         entryPart.getEntry().addExtension(doc.getRoot());
     }
 

File client-utils/src/main/java/net/chempound/client/atom/Atom.java

+package net.chempound.client.atom;
+
+/**
+ * @author Sam Adams
+ */
+public class Atom {
+
+    public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
+
+    public static final String LINK = "link";
+
+}

File client-utils/src/main/java/net/chempound/client/atom/AtomPub.java

+package net.chempound.client.atom;
+
+/**
+ * @author Sam Adams
+ */
+public class AtomPub
+{
+    public static final String ATOMPUB_NAMESPACE = "http://www.w3.org/2007/app";
+
+    static final String WORKSPACE = "workspace";
+    static final String TITLE = "title";
+
+    static final String COLLECTION = "collection";
+    static final String SERVICE = "service";
+}

File client-utils/src/main/java/net/chempound/client/sword/DepositReceipt.java

+package net.chempound.client.sword;
+
+import net.chempound.rdf.DCTerms;
+import net.sf.atomxom.model.AtomEntry;
+import net.sf.atomxom.model.AtomLink;
+import nu.xom.Document;
+import nu.xom.Element;
+import nu.xom.Elements;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.chempound.client.atom.Atom.*;
+import static net.chempound.client.sword.Sword.*;
+
+/**
+ * @author Sam Adams
+ */
+public class DepositReceipt {
+
+    private static final String REL_EDIT = "edit";
+    private static final String REL_EDIT_MEDIA = "edit-media";
+    private static final String REL_ALTERNATE = "alternate";
+
+    private static final String REL_SWORD_ADD = "http://purl.org/net/sword/terms/add";
+    private static final String REL_SWORD_STATEMENT = "http://purl.org/net/sword/terms/statement";
+    private static final String REL_SWORD_ORIGINAL_DEPOSIT = "http://purl.org/net/sword/terms/originalDeposit";
+    private static final String REL_SWORD_DERIVED_RESOURCE = "http://purl.org.net/sword/terms/derivedResource";
+
+    private final AtomEntry entry;
+
+    public DepositReceipt(final Document doc) {
+        this.entry = (AtomEntry) doc.getRootElement();
+    }
+
+    public List<Element> getDCTermsMetadata() {
+        final List<Element> list = new ArrayList<Element>();
+        final Elements elements = entry.getChildElements();
+        for (int i = 0; i  < elements.size(); i++) {
+            final Element element = elements.get(i);
+            if (DCTerms.NS.equals(element.getNamespaceURI())) {
+                list.add(element);
+            }
+        }
+        return list;
+    }
+
+    public URI getMediaEntryIRI() {
+        return getLinkIRI(REL_EDIT);
+    }
+
+    public URI getMediaResourceIRI() {
+        return getLinkIRI(REL_EDIT_MEDIA);
+    }
+
+    public URI getSwordEditIRI() {
+        return getLinkIRI(REL_SWORD_ADD);
+    }
+
+    public List<Element> getPackagingList() {
+        final List<Element> list = new ArrayList<Element>();
+        final Elements elements = entry.getChildElements(PACKAGING, SWORD_NAMESPACE);
+        for (int i = 0; i  < elements.size(); i++) {
+            list.add(elements.get(i));
+        }
+        return list;
+    }
+
+    public Element getSwordTreatment() {
+        return entry.getFirstChildElement(TREATMENT, SWORD_NAMESPACE);
+    }
+
+    public Element getSwordVerboseDescription() {
+        return entry.getFirstChildElement(VERBOSE_DESCRIPTION, SWORD_NAMESPACE);
+    }
+
+    public URI getAlternateIRI() {
+        return getLinkIRI(REL_ALTERNATE);
+    }
+
+    public URI getOriginalDepositIRI() {
+        return getLinkIRI(REL_SWORD_ORIGINAL_DEPOSIT);
+    }
+
+    public List<AtomLink> getStatementLinks() {
+        final List<AtomLink> list = new ArrayList<AtomLink>();
+        final Elements elements = entry.getChildElements(LINK, ATOM_NAMESPACE);
+        for (int i = 0; i  < elements.size(); i++) {
+            final AtomLink link = (AtomLink) elements.get(i);
+            if (REL_SWORD_STATEMENT.equals(link.getRel())) {
+                list.add(link);
+            }
+        }
+        return list;
+    }
+
+    public List<AtomLink> getDerivedResourceLinks() {
+        final List<AtomLink> list = new ArrayList<AtomLink>();
+        final Elements elements = entry.getChildElements(LINK, ATOM_NAMESPACE);
+        for (int i = 0; i  < elements.size(); i++) {
+            final AtomLink link = (AtomLink) elements.get(i);
+            if (REL_SWORD_DERIVED_RESOURCE.equals(link.getRel())) {
+                list.add(link);
+            }
+        }
+        return list;
+    }
+
+    private URI getLinkIRI(final String rel) {
+        final String href = entry.getLinkHrefByRel(rel);
+        return href == null ? null : URI.create(href);
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/sword/Sword.java

+package net.chempound.client.sword;
+
+/**
+ * @author Sam Adams
+ */
+public class Sword {
+
+    static final String SWORD_NAMESPACE = "http://purl.org/net/sword/terms/";
+
+    static final String VERSION = "version";
+    static final String PACKAGING = "packaging";
+    static final String TREATMENT = "treatment";
+    static final String MAX_UPLOAD_SIZE = "maxUploadSize";
+
+    static final String VERBOSE_DESCRIPTION = "verboseDescription";
+}

File client-utils/src/main/java/net/chempound/client/sword/SwordCollection.java

+package net.chempound.client.sword;
+
+import nu.xom.Element;
+import nu.xom.Elements;
+import nu.xom.ParentNode;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.chempound.client.atom.AtomPub.SERVICE;
+import static net.chempound.client.sword.Sword.SWORD_NAMESPACE;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordCollection {
+
+    private final Element element;
+
+    public SwordCollection(final Element element) {
+        this.element = element;
+    }
+
+    public boolean isMediation() {
+        final Element mediation = element.getFirstChildElement("mediation", SWORD_NAMESPACE);
+        return mediation != null && "true".equals(mediation.getValue().trim());
+    }
+
+    public void setMediation(final boolean value) {
+        final Element mediation = getElement("mediation", SWORD_NAMESPACE, true);
+        element.removeChildren();
+        element.appendChild(value ? "true" : "false");
+    }
+
+    private Element getElement(final String localName, final String namespaceUri, final boolean createIfMissing) {
+        Element element = this.element.getFirstChildElement(localName, namespaceUri);
+        if (element == null && createIfMissing) {
+            element = new Element(localName, namespaceUri);
+            this.element.appendChild(element);
+        }
+        return element;
+    }
+
+    public List<URI> getAcceptedPackaging() {
+        final List<URI> list = new ArrayList<URI>();
+        final Elements elements = element.getChildElements("acceptPackaging", SWORD_NAMESPACE);
+        for (int i = 0; i < elements.size(); i++) {
+            list.add(URI.create(elements.get(i).getValue().trim()));
+        }
+        return list;
+    }
+
+    public void addAcceptedPackaging(final URI packaging) {
+        final Element element = createElementInContext(this.element, "acceptPackaging", SWORD_NAMESPACE, "sword");
+        element.appendChild(packaging.toString());
+        this.element.appendChild(element);
+    }
+
+    public static Element createElementInContext(final Element context, final String localName, final String namespace, final String defaultPrefix) {
+        final String prefix = getNamespacePrefix(context, namespace, defaultPrefix);
+        return new Element((prefix == null ? localName : prefix+':'+localName), namespace);
+    }
+
+    public static String getNamespacePrefix(final Element context, final String namespace, final String defaultPrefix) {
+        Element current = context;
+        while (current != null) {
+            for (int i = current.getNamespaceDeclarationCount()-1; i >= 0; i--) {
+                final String prefix = current.getNamespacePrefix(i);
+                if (context.getNamespaceURI(prefix).equals(namespace)) {
+                    return prefix;
+                }
+            }
+            final ParentNode parent = current.getParent();
+            current = parent instanceof Element ? (Element) parent : null;
+        }
+        return defaultPrefix;
+    }
+
+    public List<URI> getServices() {
+        final List<URI> list = new ArrayList<URI>();
+        final Elements elements = element.getChildElements(SERVICE, SWORD_NAMESPACE);
+        for (int i = 0; i < elements.size(); i++) {
+            list.add(URI.create(elements.get(i).getValue()));
+        }
+        return list;
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/sword/SwordEntry.java

+package net.chempound.client.sword;
+
+import net.sf.atomxom.model.AtomEntry;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordEntry extends AtomEntry {
+
+    public SwordEntry() {
+    }
+
+    public SwordEntry(final AtomEntry old) {
+        super(old);
+    }
+
+}

File client-utils/src/main/java/net/chempound/client/sword/SwordService.java

+package net.chempound.client.sword;
+
+import nu.xom.Element;
+import nu.xom.Elements;
+import nu.xom.Serializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.chempound.client.atom.AtomPub.ATOMPUB_NAMESPACE;
+import static net.chempound.client.atom.AtomPub.WORKSPACE;
+import static net.chempound.client.sword.Sword.MAX_UPLOAD_SIZE;
+import static net.chempound.client.sword.Sword.SWORD_NAMESPACE;
+import static net.chempound.client.sword.Sword.VERSION;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordService {
+
+    private final Element service;
+
+    public SwordService(final Element service) {
+        this.service = service;
+    }
+
+    public String getVersion() {
+        final Element version = service.getFirstChildElement(VERSION, SWORD_NAMESPACE);
+        return version == null ? null : version.getValue();
+    }
+
+    public String getMaxUploadSize() {
+        final Element maxUploadSize = service.getFirstChildElement(MAX_UPLOAD_SIZE, SWORD_NAMESPACE);
+        return maxUploadSize == null ? null : maxUploadSize.getValue();
+    }
+
+    public List<SwordWorkspace> getWorkspaces() {
+        final List<SwordWorkspace> list = new ArrayList<SwordWorkspace>();
+        final Elements elements = service.getChildElements(WORKSPACE, ATOMPUB_NAMESPACE);
+        for (int i = 0; i < elements.size(); i++) {
+            list.add(new SwordWorkspace(elements.get(i)));
+        }
+        return list;
+    }
+
+    public void debug() {
+        Serializer ser = new Serializer(System.out);
+        ser.setIndent(2);
+        try {
+            ser.write(service.getDocument());
+        } catch (IOException e) {
+            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
+        }
+    }
+}

File client-utils/src/main/java/net/chempound/client/sword/SwordWorkspace.java

+package net.chempound.client.sword;
+
+import nu.xom.Element;
+import nu.xom.Elements;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.chempound.client.atom.AtomPub.*;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordWorkspace {
+
+    private final Element element;
+
+    public SwordWorkspace(final Element element) {
+        this.element = element;
+    }
+
+    public String getTitle() {
+        final Element title = element.getFirstChildElement(TITLE, ATOMPUB_NAMESPACE);
+        return title == null ? null : title.getValue();
+    }
+
+    public List<SwordCollection> getCollections() {
+        final List<SwordCollection> list = new ArrayList<SwordCollection>();
+        final Elements elements = element.getChildElements(COLLECTION, ATOMPUB_NAMESPACE);
+        for (int i = 0; i < elements.size(); i++) {
+            list.add(new SwordCollection(elements.get(i)));
+        }
+        return list;
+    }
+
+}

File client-utils/src/test/java/net/chempound/client/SwordClientTest.java

+package net.chempound.client;
+
+import com.hp.hpl.jena.vocabulary.RDF;
+import net.chempound.client.sword.DepositReceipt;
+import net.chempound.client.sword.SwordService;
+import net.chempound.content.DefaultDepositRequest;
+import net.chempound.content.DepositRequest;
+import net.chempound.rdf.CPTerms;
+import net.chempound.storage.InMemoryResource;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHttpResponse;
+import org.apache.http.message.BasicStatusLine;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Sam Adams
+ */
+public class SwordClientTest {
+
+    private static final URI REPOSITORY = URI.create("http://repo.example.net/");
+    private static final URI SERVICE_DOCUMENT = URI.create("http://repo.example.net/sword/service.xml");
+    private static final URI ROOT_COLLECTION = URI.create("http://repo.example.net/collection/");
+
+    private final HttpClient httpClient = mock(HttpClient.class);
+    private final SwordClient client = new SwordClient(httpClient);
+
+    @Test
+    public void testFindServiceDocument() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "Okay"));
+        response.setEntity(new StringEntity("<html>" +
+                "<head><link rel='http://purl.org/net/sword/terms/service-document' href='http://repo.example.net/sword/service.xml'></head>" +
+                "<body><h1>Repository</h1></body>" +
+                "</html>"));
+        when(httpClient.execute(argThat(matchesGetUri(REPOSITORY))))
+                .thenReturn(response);
+
+        assertEquals(SERVICE_DOCUMENT, client.findServiceDocumentUrl(REPOSITORY));
+    }
+
+    @Test
+    public void testFindServiceDocumentWhenNotPresent() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "Okay"));
+        response.setEntity(new StringEntity("<html>" +
+                "<head></head>" +
+                "<body><h1>Repository</h1></body>" +
+                "</html>"));
+        when(httpClient.execute(argThat(matchesGetUri(REPOSITORY))))
+                .thenReturn(response);
+
+        assertNull(client.findServiceDocumentUrl(REPOSITORY));
+    }
+
+    @Test
+    public void testGetServiceDocument() throws Exception {
+        final HttpResponse response = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "Okay"));
+        response.setEntity(new StringEntity("<service xmlns='http://www.w3.org/2007/app' />"));
+
+        when(httpClient.execute(argThat(matchesGetUri(SERVICE_DOCUMENT))))
+                .thenReturn(response);
+
+        assertNotNull(client.getServiceDocument(SERVICE_DOCUMENT));