fedora.test.api.TestRESTAPI.java Source code

Java tutorial

Introduction

Here is the source code for fedora.test.api.TestRESTAPI.java

Source

/* The contents of this file are subject to the license and copyright terms
 * detailed in the license directory at the root of the source tree (also
 * available online at http://fedora-commons.org/license/).
 */
package fedora.test.api;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import java.net.URL;
import java.net.URLEncoder;

import java.text.SimpleDateFormat;

import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;

import org.junit.Test;

import junit.framework.TestSuite;

import fedora.common.PID;

import fedora.server.access.FedoraAPIA;
import fedora.server.management.FedoraAPIM;
import fedora.server.types.gen.Datastream;
import fedora.server.types.gen.MIMETypedStream;

import fedora.test.DemoObjectTestSetup;
import fedora.test.FedoraServerTestCase;

import static org.apache.commons.httpclient.HttpStatus.SC_CREATED;
import static org.apache.commons.httpclient.HttpStatus.SC_INTERNAL_SERVER_ERROR;
import static org.apache.commons.httpclient.HttpStatus.SC_NOT_FOUND;
import static org.apache.commons.httpclient.HttpStatus.SC_NO_CONTENT;
import static org.apache.commons.httpclient.HttpStatus.SC_OK;
import static org.apache.commons.httpclient.HttpStatus.SC_UNAUTHORIZED;

/**
 * Tests of the REST API. Tests assume a running instance of Fedora with the
 * REST API enabled. //TODO: actually validate the ResponseBody instead of just
 * HTTP status codes
 *
 * @author Edwin Shin
 * @author Bill Branan
 * @since 3.0
 * @version $Id$
 */
public class TestRESTAPI extends FedoraServerTestCase {

    private FedoraAPIA apia;

    private FedoraAPIM apim;

    private static String DEMO_REST;

    private static byte[] DEMO_REST_FOXML;

    private static String DEMO_MIN;

    private final PID pid = PID.getInstance("demo:REST");

    // various download filenames used in test object for content-disposition header test
    // datastreams in test object (using these) are:
    // DS1 with label; also has relationship in RELS-INT specifying filename
    // DS2 with label - MIMETYPE maps to an extension
    // DS3 with label - unknown MIMETYPE
    // DS4 with label containing illegal filename characters
    // DS5 with no label
    // DS6.xml with no label; datastream ID contains extension
    //
    // all datastreams text/xml apart from DS2 which is image/jpeg

    private static String DS1RelsFilename = "Datastream 1 filename from rels.extension";
    private static String DS2LabelFilename = "Datastream 2 filename from label";
    private static String DS3LabelFilename = "Datastream 3 filename from label";
    private static String DodgyChars = "\\/*?<>:|";
    private static String DS4LabelFilenameOriginal = "Datastream 4 filename " + DodgyChars + "from label"; // this one in foxml
    private static String DS4LabelFilename = "Datastream 4 filename from label"; // this should be the cleaned version
    private static String DS5ID = "DS5";
    private static String DS6ID = "DS6.xml";

    protected String url;

    private static final String datetime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(new Date());

    private boolean chunked = false;

    static {
        // TODO:  RELS-INT relationship from MODEL, not text
        // Test FOXML object with RELS-EXT datastream
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        sb.append("<foxml:digitalObject VERSION=\"1.1\" PID=\"demo:REST\" ");
        sb.append("  xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\" ");
        sb.append("  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
        sb.append("  xsi:schemaLocation=\"info:fedora/fedora-system:def/foxml# ");
        sb.append("  http://www.fedora.info/definitions/1/0/foxml1-1.xsd\">");
        sb.append("  <foxml:objectProperties>");
        sb.append("    <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\"A\"/>");
        sb.append("  </foxml:objectProperties>");
        sb.append("  <foxml:datastream ID=\"DC\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append(
                "    <foxml:datastreamVersion FORMAT_URI=\"http://www.openarchives.org/OAI/2.0/oai_dc/\" ID=\"DC1.0\" MIMETYPE=\"text/xml\" LABEL=\"Dublin Core Record for this object\">");
        sb.append("      <foxml:xmlContent>");
        sb.append(
                "        <oai_dc:dc xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:oai_dc=\"http://www.openarchives.org/OAI/2.0/oai_dc/\">");
        sb.append("          <dc:title>Coliseum in Rome</dc:title>");
        sb.append("          <dc:creator>Thornton Staples</dc:creator>");
        sb.append("          <dc:subject>Architecture, Roman</dc:subject>");
        sb.append("          <dc:description>Image of Coliseum in Rome</dc:description>");
        sb.append("          <dc:publisher>University of Virginia Library</dc:publisher>");
        sb.append("          <dc:format>image/jpeg</dc:format>");
        sb.append("          <dc:identifier>demo:REST</dc:identifier>");
        sb.append("        </oai_dc:dc>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        sb.append("  <foxml:datastream ID=\"RELS-EXT\" CONTROL_GROUP=\"M\" STATE=\"A\">");
        sb.append(
                "    <foxml:datastreamVersion FORMAT_URI=\"info:fedora/fedora-system:FedoraRELSExt-1.0\" ID=\"RELS-EXT.0\" MIMETYPE=\"application/rdf+xml\" LABEL=\"RDF Statements about this object\" CREATED=\""
                        + datetime + "\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
                + "                 xmlns:rel=\"info:fedora/fedora-system:def/relations-external#\">");
        sb.append("          <rdf:Description rdf:about=\"info:fedora/demo:REST\">");
        sb.append("            <rel:hasFormalContentModel rdf:resource=\"info:fedora/demo:UVA_STD_IMAGE_1\"/>");
        sb.append("          </rdf:Description>");
        sb.append("        </rdf:RDF>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        sb.append("  <foxml:datastream ID=\"DS1\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS1.0\" MIMETYPE=\"text/xml\" LABEL=\"Datastream 1\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        sb.append("  <foxml:datastream CONTROL_GROUP=\"E\" ID=\"EXTDS\" STATE=\"A\" VERSIONABLE=\"true\">");
        sb.append("    <foxml:datastreamVersion ID=\"EXTDS1.0\" LABEL=\"External\" MIMETYPE=\"text/xml\">");
        sb.append("      <foxml:contentLocation REF=\"" + getBaseURL() + "/get/demo:REST/DS1\" TYPE=\"URL\"/>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        // datastreams for content-disposition header (get datastream filename) testing
        // RELS-INT: specifies filename for DS1
        sb.append("  <foxml:datastream ID=\"RELS-INT\" CONTROL_GROUP=\"M\" STATE=\"A\">");
        sb.append(
                "    <foxml:datastreamVersion FORMAT_URI=\"info:fedora/fedora-system:FedoraRELSInt-1.0\" ID=\"RELS-INT.0\" MIMETYPE=\"application/rdf+xml\" LABEL=\"RDF Statements about datastreams in this object\" CREATED=\""
                        + datetime + "\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
                + "                 xmlns:fedora-model=\"" + MODEL.uri + "\">");
        sb.append("          <rdf:Description rdf:about=\"info:fedora/demo:REST/DS1\">");
        sb.append("            <fedora-model:" + MODEL.DOWNLOAD_FILENAME.localName + ">" + DS1RelsFilename
                + "</fedora-model:" + MODEL.DOWNLOAD_FILENAME.localName + ">");
        sb.append("          </rdf:Description>");
        sb.append("        </rdf:RDF>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");

        // DS2:  label is filename, known mimetype of image/jpeg so extension (jpg) should be determined by mappings file
        sb.append("  <foxml:datastream ID=\"DS2\" CONTROL_GROUP=\"M\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS2.0\" MIMETYPE=\"image/jpeg\" LABEL=\"" + DS2LabelFilename
                + "\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");

        // DS3:  label is filename, mimetype unknown so extension should be default
        sb.append("  <foxml:datastream ID=\"DS3\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS3.0\" MIMETYPE=\"unknown/mimetype\" LABEL=\""
                + DS3LabelFilename + "\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");

        // DS4: label is filename, with illegal filename characters
        sb.append("  <foxml:datastream ID=\"DS4\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS4.0\" MIMETYPE=\"text/xml\" LABEL=\""
                + DS4LabelFilenameOriginal + "\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");

        //DS5: no label, ID is filename
        sb.append("  <foxml:datastream ID=\"DS5\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS5.0\" MIMETYPE=\"text/xml\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");

        // DS6: no label, ID is filename plus extension
        sb.append("  <foxml:datastream ID=\"DS6.xml\" CONTROL_GROUP=\"X\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS6.0\" MIMETYPE=\"text/xml\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        sb.append("</foxml:digitalObject>");

        try {
            DEMO_REST = sb.toString();
            DEMO_REST_FOXML = DEMO_REST.getBytes("UTF-8");
        } catch (UnsupportedEncodingException uee) {
        }

        // Test minimal FOXML object:  No PID, one managed datastream
        sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        sb.append("<foxml:digitalObject VERSION=\"1.1\" ");
        sb.append("  xmlns:foxml=\"info:fedora/fedora-system:def/foxml#\" ");
        sb.append("  xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
        sb.append("  xsi:schemaLocation=\"info:fedora/fedora-system:def/foxml# ");
        sb.append("  http://www.fedora.info/definitions/1/0/foxml1-1.xsd\">");
        sb.append("  <foxml:objectProperties>");
        sb.append("    <foxml:property NAME=\"info:fedora/fedora-system:def/model#state\" VALUE=\"A\"/>");
        sb.append("  </foxml:objectProperties>");
        sb.append("  <foxml:datastream ID=\"DS1\" CONTROL_GROUP=\"M\" STATE=\"A\">");
        sb.append("    <foxml:datastreamVersion ID=\"DS1.0\" MIMETYPE=\"text/xml\" LABEL=\"Datastream 1\">");
        sb.append("      <foxml:xmlContent>");
        sb.append("        <foo>");
        sb.append("          <bar>baz</bar>");
        sb.append("        </foo>");
        sb.append("      </foxml:xmlContent>");
        sb.append("    </foxml:datastreamVersion>");
        sb.append("  </foxml:datastream>");
        sb.append("</foxml:digitalObject>");

        DEMO_MIN = sb.toString();

    }

    @Override
    public void setUp() throws Exception {
        apia = getFedoraClient().getAPIA();
        apim = getFedoraClient().getAPIM();
        apim.ingest(DEMO_REST_FOXML, FOXML1_1.uri, "ingesting new foxml object");

    }

    @Override
    public void tearDown() throws Exception {
        apim.purgeObject(pid.toString(), "", false);
    }

    @Test
    public void testGetWADL() throws Exception {
        url = "/objects/application.wadl";

        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    //public void testDescribeRepository() throws Exception {}

    // API-A
    @Test
    public void testGetObjectProfile() throws Exception {
        url = String.format("/objects/%s", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        String responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("<objLabel>"));
        assertTrue(responseXML.contains("<objOwnerId>"));
        assertTrue(responseXML.contains("<objCreateDate>"));
        assertTrue(responseXML.contains("<objLastModDate>"));
        assertTrue(responseXML.contains("<objDissIndexViewURL>"));
        assertTrue(responseXML.contains("<objItemIndexViewURL>"));
        assertTrue(responseXML.contains("<objState>"));

        url = String.format("/objects/%s?asOfDateTime=%s", pid.toString(), datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        // sanity check
        url = String.format("/objects/%s", "demo:BOGUS_PID");
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());
    }

    public void testListMethods() throws Exception {
        url = String.format("/objects/%s/methods", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/methods?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/methods?asOfDateTime=%s", pid.toString(), datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testListMethodsForSDep() throws Exception {
        url = String.format("/objects/%s/methods/fedora-system:3", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/methods/fedora-system:3?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        String responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("sDef=\"fedora-system:3\""));

        url = String.format("/objects/%s/methods/fedora-system:3?asOfDateTime=%s", pid.toString(), datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    //
    // GETs on built-in Service Definition methods
    //

    public void testGETMethodBuiltInBadMethod() throws Exception {
        url = String.format("/objects/%s/methods/fedora-system:3/noSuchMethod", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodBuiltInBadUserArg() throws Exception {
        url = String.format("/objects/%s/methods/fedora-system:3/viewMethodIndex?noSuchArg=foo", pid.toString());
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodBuiltInNoArg() throws Exception {
        url = String.format("/objects/%s/methods/fedora-system:3/viewMethodIndex", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    //
    // GETs on custom Service Definition methods
    //

    public void testGETMethodCustomBadMethod() throws Exception {
        url = "/objects/demo:14/methods/demo:12/noSuchMethod";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodCustomBadUserArg() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?noSuchArg=foo";
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodCustomNoArg() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testGETMethodCustomGoodUserArg() throws Exception {
        url = "/objects/demo:29/methods/demo:27/resizeImage?width=50";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        assertEquals(1486, response.getResponseBody().length);
        assertEquals("image/jpeg", response.getResponseHeader("Content-Type").getValue());
    }

    public void testGETMethodCustomGoodUserArgGoodDate() throws Exception {
        url = "/objects/demo:29/methods/demo:27/resizeImage?width=50&asOfDateTime=" + datetime;
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        assertEquals(1486, response.getResponseBody().length);
        assertEquals("image/jpeg", response.getResponseHeader("Content-Type").getValue());
    }

    public void testGETMethodCustomUserArgBadDate() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?width=50&asOfDateTime=badDate";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodCustomUserArgEarlyDate() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?width=50&asOfDateTime=1999-11-21T16:38:32.200Z";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());
    }

    public void testGETMethodCustomGoodDate() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?asOfDateTime=" + datetime;
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testGETMethodCustomBadDate() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?asOfDateTime=badDate";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertFalse(SC_OK == get(true).getStatusCode());
    }

    public void testGETMethodCustomEarlyDate() throws Exception {
        url = "/objects/demo:14/methods/demo:12/getDocumentStyle1?asOfDateTime=1999-11-21T16:38:32.200Z";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());
    }

    public void testListDatastreams() throws Exception {
        url = String.format("/objects/%s/datastreams", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/datastreams?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/datastreams?asOfDateTime=%s", pid.toString(), datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testGetDatastreamProfile() throws Exception {
        // Datastream profile in HTML format
        url = String.format("/objects/%s/datastreams/RELS-EXT", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        String responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("<html>"));

        // Datastream profile in XML format
        url = String.format("/objects/%s/datastreams/RELS-EXT?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("<dsLabel>"));

        // Datastream profile as of the current date-time (XML format)
        url = String.format("/objects/%s/datastreams/RELS-EXT?asOfDateTime=%s&format=xml", pid.toString(),
                datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("<dsLabel>"));

        // sanity check
        url = String.format("/objects/%s/datastreams/BOGUS_DSID", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());
    }

    public void testGetDatastreamDissemination() throws Exception {
        url = String.format("/objects/%s/datastreams/RELS-EXT/content", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        String responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("rdf:Description"));

        url = String.format("/objects/%s/datastreams/RELS-EXT/content?asOfDateTime=%s", pid.toString(), datetime);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.contains("rdf:Description"));

        // sanity check
        url = String.format("/objects/%s/datastreams/BOGUS_DSID/content", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());
    }

    public void testFindObjects() throws Exception {
        url = String.format("/objects?pid=true&terms=%s&query=&resultFormat=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testResumeFindObjects() throws Exception {
        url = "/objects?pid=true&query=&resultFormat=xml";
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());

        String responseXML = new String(response.responseBody, "UTF-8");
        String sessionToken = responseXML.substring(responseXML.indexOf("<token>") + 7,
                responseXML.indexOf("</token>"));

        url = String.format("/objects?pid=true&query=&format=xml&sessionToken=%s", sessionToken);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testGetObjectHistory() throws Exception {
        url = String.format("/objects/%s/versions", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/versions?format=xml", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    private String extractPid(String source) {
        Matcher m = Pattern.compile("^.*/([^/]+$)").matcher(source);
        String pid = null;
        if (m.find() && m.groupCount() == 1) {
            pid = m.group(1);
        }
        return pid;
    }

    // API-M
    public void testIngest() throws Exception {
        // Create new empty object
        url = String.format("/objects/new");
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        HttpResponse response = post("", true);
        assertEquals(SC_CREATED, response.getStatusCode());

        String emptyObjectPid = extractPid(response.responseHeaders[1].toString());
        assertNotNull(emptyObjectPid);
        emptyObjectPid = emptyObjectPid.replaceAll("\n", "").replaceAll("\r", "").replaceAll("%3A", ":");
        // PID should be returned as a header and as the response body
        String responseBody = new String(response.getResponseBody(), "UTF-8");
        assertTrue(responseBody.equals(emptyObjectPid));

        // Delete empty object
        url = String.format("/objects/%s", emptyObjectPid);
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());

        // Ensure that GETs of the deleted object immediately give 404s (See FCREPO-594)
        assertEquals(SC_NOT_FOUND, get(true).getStatusCode());

        // Create new empty object with a PID namespace specified
        url = String.format("/objects/new?namespace=test");
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        response = post("", true);
        assertEquals(SC_CREATED, response.getStatusCode());

        emptyObjectPid = extractPid(response.responseHeaders[1].toString());
        assertTrue(emptyObjectPid.startsWith("test"));

        // Delete empty "test" object
        url = String.format("/objects/%s", emptyObjectPid);
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());

        // Delete the demo:REST object (ingested as part of setup)
        url = String.format("/objects/%s", pid.toString());
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());

        // Ingest the demo:REST object
        url = String.format("/objects/%s", pid.toString());
        assertEquals(SC_UNAUTHORIZED, post(DEMO_REST, false).getStatusCode());
        response = post(DEMO_REST, true);
        assertEquals(SC_CREATED, response.getStatusCode());
        Header locationHeader = response.getResponseHeader("location");
        assertNotNull(locationHeader);
        assertTrue(locationHeader.getValue().contains(URLEncoder.encode(pid.toString(), "UTF-8")));
        responseBody = new String(response.getResponseBody(), "UTF-8");
        assertTrue(responseBody.equals(pid.toString()));

        // Ingest minimal object with no PID
        url = String.format("/objects/new");
        assertEquals(SC_UNAUTHORIZED, post(DEMO_MIN, false).getStatusCode());
        response = post(DEMO_MIN, true);
        assertEquals(SC_CREATED, response.getStatusCode());

        // Delete minimal object
        String minimalObjectPid = extractPid(response.responseHeaders[1].toString());
        url = String.format("/objects/%s", minimalObjectPid);
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());
    }

    public void testModifyObject() throws Exception {
        url = String.format("/objects/%s?label=%s", pid.toString(), "foo");
        assertEquals(SC_UNAUTHORIZED, put("", false).getStatusCode());
        HttpResponse response = put("", true);
        assertEquals(SC_OK, response.getStatusCode());
        assertEquals("foo", apia.getObjectProfile(pid.toString(), null).getObjLabel());
    }

    public void testGetObjectXML() throws Exception {
        url = String.format("/objects/%s/objectXML", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testExportObject() throws Exception {
        url = String.format("/objects/%s/export", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());

        url = String.format("/objects/%s/export?context=public", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        assertEquals(SC_OK, get(true).getStatusCode());
    }

    public void testPurgeObject() throws Exception {
        url = String.format("/objects/%s", "demo:TEST_PURGE");
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        assertEquals(SC_CREATED, post("", true).getStatusCode());
        url = String.format("/objects/demo:TEST_PURGE");
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());
    }

    public void testAddDatastream() throws Exception {
        // inline (X) datastream
        String xmlData = "<foo>bar</foo>";
        String dsPath = "/objects/" + pid + "/datastreams/FOO";
        url = dsPath + "?controlGroup=X&dsLabel=bar";
        assertEquals(SC_UNAUTHORIZED, post(xmlData, false).getStatusCode());
        HttpResponse response = post(xmlData, true);
        assertEquals(SC_CREATED, response.getStatusCode());
        Header locationHeader = response.getResponseHeader("location");
        assertNotNull(locationHeader);
        assertEquals(new URL(url.substring(0, url.indexOf('?'))).toString(), locationHeader.getValue());
        assertEquals("text/xml", response.getResponseHeader("Content-Type").getValue());
        url = dsPath + "?format=xml";
        assertEquals(response.getResponseBodyString(), get(true).getResponseBodyString());

        // managed (M) datastream
        String mimeType = "text/plain";
        dsPath = "/objects/" + pid + "/datastreams/BAR";
        url = dsPath + "?controlGroup=M&dsLabel=bar&mimeType=" + mimeType;
        File temp = File.createTempFile("test", null);
        DataOutputStream os = new DataOutputStream(new FileOutputStream(temp));
        os.write(42);
        os.close();
        assertEquals(SC_UNAUTHORIZED, post(temp, false).getStatusCode());
        response = post(temp, true);
        assertEquals(SC_CREATED, response.getStatusCode());
        locationHeader = response.getResponseHeader("location");
        assertNotNull(locationHeader);
        assertEquals(new URL(url.substring(0, url.indexOf('?'))).toString(), locationHeader.getValue());
        assertEquals("text/xml", response.getResponseHeader("Content-Type").getValue());
        url = dsPath + "?format=xml";
        assertEquals(response.getResponseBodyString(), get(true).getResponseBodyString());
        Datastream ds = apim.getDatastream(pid.toString(), "BAR", null);
        assertEquals(ds.getMIMEType(), mimeType);
    }

    public void testModifyDatastreamByReference() throws Exception {
        // Create BAR datastream
        url = String.format("/objects/%s/datastreams/BAR?controlGroup=M&dsLabel=bar", pid.toString());
        File temp = File.createTempFile("test", null);
        DataOutputStream os = new DataOutputStream(new FileOutputStream(temp));
        os.write(42);
        os.close();
        assertEquals(SC_UNAUTHORIZED, post(temp, false).getStatusCode());
        assertEquals(SC_CREATED, post(temp, true).getStatusCode());

        // Update the content of the BAR datastream (using PUT)
        url = String.format("/objects/%s/datastreams/BAR", pid.toString());
        assertEquals(SC_UNAUTHORIZED, put(temp, false).getStatusCode());
        HttpResponse response = put(temp, true);
        assertEquals(SC_OK, response.getStatusCode());
        assertEquals("text/xml", response.getResponseHeader("Content-Type").getValue());
        url = url + "?format=xml";
        assertEquals(response.getResponseBodyString(), get(true).getResponseBodyString());

        // Ensure 404 on attempt to update BOGUS_DS via PUT
        url = "/objects/" + pid + "/datastreams/BOGUS_DS";
        assertEquals(SC_NOT_FOUND, put(temp, true).getStatusCode());

        // Update the content of the BAR datastream (using POST)
        url = String.format("/objects/%s/datastreams/BAR", pid.toString());
        response = post(temp, true);
        assertEquals(SC_CREATED, response.getStatusCode());
        Header locationHeader = response.getResponseHeader("location");
        assertNotNull(locationHeader);
        assertEquals(url, locationHeader.getValue());
        assertEquals("text/xml", response.getResponseHeader("Content-Type").getValue());
        url = url + "?format=xml";
        assertEquals(response.getResponseBodyString(), get(true).getResponseBodyString());

        // Update the label of the BAR datastream
        String newLabel = "tikibar";
        url = String.format("/objects/%s/datastreams/BAR?dsLabel=%s", pid.toString(), newLabel);
        assertEquals(SC_UNAUTHORIZED, put(false).getStatusCode());
        assertEquals(SC_OK, put(true).getStatusCode());
        assertEquals(newLabel, apim.getDatastream(pid.toString(), "BAR", null).getLabel());

        // Update the location of the EXTDS datastream (E type datastream)
        String newLocation = "http://" + getHost() + ":" + getPort() + "/" + getFedoraAppServerContext()
                + "/get/demo:REST/DC";
        url = String.format("/objects/%s/datastreams/EXTDS?dsLocation=%s", pid.toString(), newLocation);
        assertEquals(SC_UNAUTHORIZED, put(false).getStatusCode());
        assertEquals(SC_OK, put(true).getStatusCode());

        assertEquals(newLocation, apim.getDatastream(pid.toString(), "EXTDS", null).getLocation());
        String dcDS = new String(apia.getDatastreamDissemination(pid.toString(), "DC", null).getStream());
        String extDS = new String(apia.getDatastreamDissemination(pid.toString(), "EXTDS", null).getStream());
        assertEquals(dcDS, extDS);

        // Update DS1 by reference (X type datastream)
        // Error expected because attempting to access internal DS with API-A auth on
        url = String.format("/objects/%s/datastreams/DS1?dsLocation=%s", pid.toString(), newLocation);
        assertEquals(SC_UNAUTHORIZED, put(false).getStatusCode());
        assertEquals(SC_INTERNAL_SERVER_ERROR, put(true).getStatusCode());

        // Update DS1 by reference (X type datastream) - Success expected
        newLocation = getBaseURL() + "/ri/index.xsl";
        url = String.format("/objects/%s/datastreams/DS1?dsLocation=%s", pid.toString(), newLocation);
        assertEquals(SC_UNAUTHORIZED, put(false).getStatusCode());
        assertEquals(SC_OK, put(true).getStatusCode());
    }

    public void testModifyDatastreamByValue() throws Exception {
        String xmlData = "<baz>quux</baz>";
        url = String.format("/objects/%s/datastreams/DS1", pid.toString());

        assertEquals(SC_UNAUTHORIZED, put(xmlData, false).getStatusCode());
        assertEquals(SC_OK, put(xmlData, true).getStatusCode());

        MIMETypedStream ds1 = apia.getDatastreamDissemination(pid.toString(), "DS1", null);
        assertXMLEqual(xmlData, new String(ds1.getStream(), "UTF-8"));
    }

    public void testModifyDatastreamNoContent() throws Exception {
        String label = "Label";
        url = String.format("/objects/%s/datastreams/DS1?dsLabel=%s", pid.toString(), label);

        assertEquals(SC_UNAUTHORIZED, put("", false).getStatusCode());
        assertEquals(SC_OK, put("", true).getStatusCode());

        Datastream ds1 = apim.getDatastream(pid.toString(), "DS1", null);
        assertEquals(label, ds1.getLabel());
    }

    public void testSetDatastreamState() throws Exception {
        String state = "D";
        url = String.format("/objects/%s/datastreams/DS1?dsState=%s", pid.toString(), state);
        assertEquals(SC_UNAUTHORIZED, put("", false).getStatusCode());
        assertEquals(SC_OK, put("", true).getStatusCode());

        Datastream ds1 = apim.getDatastream(pid.toString(), "DS1", null);
        assertEquals(state, ds1.getState());
    }

    public void testSetDatastreamVersionable() throws Exception {
        boolean versionable = false;
        url = String.format("/objects/%s/datastreams/DS1?versionable=%s", pid.toString(), versionable);
        assertEquals(SC_UNAUTHORIZED, put("", false).getStatusCode());
        assertEquals(SC_OK, put("", true).getStatusCode());

        Datastream ds1 = apim.getDatastream(pid.toString(), "DS1", null);
        assertEquals(versionable, ds1.isVersionable());
    }

    public void testPurgeDatastream() throws Exception {
        url = String.format("/objects/%s/datastreams/RELS-EXT", pid.toString());
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());
    }

    public void testGetNextPID() throws Exception {
        url = "/objects/nextPID";
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        assertEquals(SC_OK, post("", true).getStatusCode());
    }

    public void testLifecycle() throws Exception {
        HttpResponse response = null;

        // Get next PID
        url = "/objects/nextPID?format=xml";
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        response = post("", true);
        assertEquals(SC_OK, response.getStatusCode());

        String responseXML = new String(response.responseBody, "UTF-8");
        String pid = responseXML.substring(responseXML.indexOf("<pid>") + 5, responseXML.indexOf("</pid>"));

        // Ingest object
        String label = "Lifecycle-Test-Label";
        url = String.format("/objects/%s?label=%s", pid, label);
        assertEquals(SC_UNAUTHORIZED, post("", false).getStatusCode());
        assertEquals(SC_CREATED, post("", true).getStatusCode());

        // Add datastream
        String datastreamData = "<test>Test Datastream</test>";
        url = String.format("/objects/%s/datastreams/TESTDS?controlGroup=X&dsLabel=Test", pid.toString());
        assertEquals(SC_UNAUTHORIZED, post(datastreamData, false).getStatusCode());
        assertEquals(SC_CREATED, post(datastreamData, true).getStatusCode());

        // Get object XML
        url = String.format("/objects/%s/objectXML", pid);
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.indexOf(label) > 0);
        assertTrue(responseXML.indexOf(datastreamData) > 0);

        // Modify object
        label = "Updated-Label";
        url = String.format("/objects/%s?label=%s", pid.toString(), label);
        assertEquals(SC_UNAUTHORIZED, put("", false).getStatusCode());
        assertEquals(SC_OK, put("", true).getStatusCode());

        // Modify datastream
        datastreamData = "<test>Update Test</test>";
        url = String.format("/objects/%s/datastreams/TESTDS", pid.toString());
        assertEquals(SC_UNAUTHORIZED, put(datastreamData, false).getStatusCode());
        assertEquals(SC_OK, put(datastreamData, true).getStatusCode());

        // Export
        url = String.format("/objects/%s/export", pid.toString());
        assertEquals(SC_UNAUTHORIZED, get(false).getStatusCode());
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        responseXML = new String(response.responseBody, "UTF-8");
        assertTrue(responseXML.indexOf(label) > 0);
        assertTrue(responseXML.indexOf(datastreamData) > 0);

        // Purge datastream
        url = String.format("/objects/%s/datastreams/TESTDS", pid);
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());

        // Purge object
        url = String.format("/objects/%s", pid);
        assertEquals(SC_UNAUTHORIZED, delete(false).getStatusCode());
        assertEquals(SC_NO_CONTENT, delete(true).getStatusCode());
    }

    public void testChunked() throws Exception {
        chunked = true;
        testIngest();
        testModifyDatastreamByValue();
        testModifyDatastreamNoContent();
        testLifecycle();
    }

    public void testResponseOverride() throws Exception {
        // Make request which returns error response
        url = String.format("/objects/%s", "BOGUS_PID");
        assertEquals(SC_INTERNAL_SERVER_ERROR, post("", true).getStatusCode());

        // With flash=true parameter response should be 200
        url = String.format("/objects/%s?flash=true", "BOGUS_PID");
        assertEquals(SC_OK, post("", true).getStatusCode());
    }

    // test correct content-disposition header on getDatastreamDissemination
    // Note that these tests are dependent on the following configuration in fedora.fcfg
    // Datastream filename sources: rels, label, id
    // Datastream extension preferences:
    // rels: never (filename always sourced from relationship)
    // label: always (filename always determined from mime-type to extension mapping)
    // id: ifmissing (filename sourced from mapping if none present in datastream id)

    @Test
    public void testDatastreamDisseminationContentDispositionFromRels() throws Exception {

        // filename from RELS-INT, no lookup of extension; no download
        url = "/objects/demo:REST/datastreams/DS1/content";
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "inline", TestRESTAPI.DS1RelsFilename);
        // again with download
        url = url + "?download=true";
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS1RelsFilename);
    }

    @Test
    public void testDatastreamDisseminationContentDispositionFromLabel() throws Exception {

        // filename from label, known MIMETYPE
        url = "/objects/demo:REST/datastreams/DS2/content?download=true";
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS2LabelFilename + ".jpg"); // jpg should be from MIMETYPE mapping

        // filename from label, unknown MIMETYPE
        url = "/objects/demo:REST/datastreams/DS3/content?download=true";
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS3LabelFilename + ".bin"); // default extension from config

        // filename from label with illegal characters, known MIMETYPE
        url = "/objects/demo:REST/datastreams/DS4/content?download=true";
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS4LabelFilename + ".xml"); // xml from mimetype mapping
    }

    @Test
    public void testDatastreamDisseminationContentDispositionFromId() throws Exception {

        // filename from id (no label present)
        url = "/objects/demo:REST/datastreams/DS5/content?download=true";
        HttpResponse response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS5ID + ".xml"); // xml from mimetype mapping

        // filename from id, id contains extension (no label present)
        url = "/objects/demo:REST/datastreams/DS6.xml/content?download=true";
        response = get(true);
        assertEquals(SC_OK, response.getStatusCode());
        CheckCDHeader(response, "attachment", TestRESTAPI.DS6ID); // no extension, id contains it

    }

    // check content disposition header of response
    private void CheckCDHeader(HttpResponse response, String expectedType, String expectedFilename) {
        String contentDisposition = "";
        Header[] headers = response.responseHeaders;
        for (Header header : headers) {
            if (header.getName().equals("content-disposition")) {
                contentDisposition = header.getValue();
            }
        }
        assertEquals(expectedType + "; " + "filename=\"" + expectedFilename + "\"", contentDisposition);
    }

    // helper methods
    private HttpClient getClient(boolean auth) {
        HttpClient client = new HttpClient();
        client.getParams().setAuthenticationPreemptive(true);
        if (auth) {
            client.getState().setCredentials(new AuthScope(getHost(), Integer.valueOf(getPort()), "realm"),
                    new UsernamePasswordCredentials(getUsername(), getPassword()));
        }
        return client;
    }

    /**
     * Issues an HTTP GET for the specified URL.
     *
     * @param url
     *        The URL to GET: either an absolute URL or URL relative to the
     *        Fedora webapp (e.g. "/objects/demo:10").
     * @param authenticate
     * @return HttpResponse
     * @throws Exception
     */
    protected HttpResponse get(boolean authenticate) throws Exception {
        return getOrDelete("GET", authenticate);
    }

    protected HttpResponse delete(boolean authenticate) throws Exception {
        return getOrDelete("DELETE", authenticate);
    }

    /**
     * Issues an HTTP PUT to <code>url</code>. Callers are responsible for
     * calling releaseConnection() on the returned <code>HttpMethod</code>.
     *
     * @param requestContent
     * @param authenticate
     * @return
     * @throws Exception
     */
    protected HttpResponse put(boolean authenticate) throws Exception {
        return putOrPost("PUT", null, authenticate);
    }

    protected HttpResponse put(String requestContent, boolean authenticate) throws Exception {
        return putOrPost("PUT", requestContent, authenticate);
    }

    protected HttpResponse post(String requestContent, boolean authenticate) throws Exception {
        return putOrPost("POST", requestContent, authenticate);
    }

    protected HttpResponse put(File requestContent, boolean authenticate) throws Exception {
        return putOrPost("PUT", requestContent, authenticate);
    }

    protected HttpResponse post(File requestContent, boolean authenticate) throws Exception {
        return putOrPost("POST", requestContent, authenticate);
    }

    private HttpResponse getOrDelete(String method, boolean authenticate) throws Exception {
        if (url == null || url.length() == 0) {
            throw new IllegalArgumentException("url must be a non-empty value");
        } else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
            url = getBaseURL() + url;
        }
        HttpMethod httpMethod = null;
        try {
            if (method.equals("GET")) {
                httpMethod = new GetMethod(url);
            } else if (method.equals("DELETE")) {
                httpMethod = new DeleteMethod(url);
            } else {
                throw new IllegalArgumentException("method must be one of GET or DELETE.");
            }

            httpMethod.setDoAuthentication(authenticate);
            httpMethod.getParams().setParameter("Connection", "Keep-Alive");
            getClient(authenticate).executeMethod(httpMethod);
            return new HttpResponse(httpMethod);
        } finally {
            if (httpMethod != null) {
                httpMethod.releaseConnection();
            }
        }
    }

    private HttpResponse putOrPost(String method, Object requestContent, boolean authenticate) throws Exception {
        if (url == null || url.length() == 0) {
            throw new IllegalArgumentException("url must be a non-empty value");
        } else if (!(url.startsWith("http://") || url.startsWith("https://"))) {
            url = getBaseURL() + url;
        }

        EntityEnclosingMethod httpMethod = null;
        try {
            if (method.equals("PUT")) {
                httpMethod = new PutMethod(url);
            } else if (method.equals("POST")) {
                httpMethod = new PostMethod(url);
            } else {
                throw new IllegalArgumentException("method must be one of PUT or POST.");
            }

            httpMethod.setDoAuthentication(authenticate);
            httpMethod.getParams().setParameter("Connection", "Keep-Alive");
            if (requestContent != null) {
                httpMethod.setContentChunked(chunked);
                if (requestContent instanceof String) {
                    httpMethod.setRequestEntity(
                            new StringRequestEntity((String) requestContent, "text/xml", "utf-8"));
                } else if (requestContent instanceof File) {
                    Part[] parts = { new StringPart("param_name", "value"),
                            new FilePart(((File) requestContent).getName(), (File) requestContent) };
                    httpMethod.setRequestEntity(new MultipartRequestEntity(parts, httpMethod.getParams()));
                } else {
                    throw new IllegalArgumentException("requestContent must be a String or File");
                }
            }
            getClient(authenticate).executeMethod(httpMethod);
            return new HttpResponse(httpMethod);
        } finally {
            if (httpMethod != null) {
                httpMethod.releaseConnection();
            }
        }
    }

    // Supports legacy test runners
    public static junit.framework.Test suite() {
        TestSuite suite = new TestSuite("REST API TestSuite");
        suite.addTestSuite(TestRESTAPI.class);
        return new DemoObjectTestSetup(suite);
    }

    class HttpResponse {

        private final int statusCode;

        private final byte[] responseBody;

        private final Header[] responseHeaders;

        private final Header[] responseFooters;

        HttpResponse(int status, byte[] body, Header[] headers, Header[] footers) {
            statusCode = status;
            responseBody = body;
            responseHeaders = headers;
            responseFooters = footers;
        }

        HttpResponse(HttpMethod method) throws IOException {
            statusCode = method.getStatusCode();
            responseBody = method.getResponseBody();
            responseHeaders = method.getResponseHeaders();
            responseFooters = method.getResponseFooters();
        }

        public int getStatusCode() {
            return statusCode;
        }

        public byte[] getResponseBody() {
            return responseBody;
        }

        public String getResponseBodyString() {
            try {
                return new String(responseBody, "UTF-8");
            } catch (UnsupportedEncodingException wontHappen) {
                throw new Error(wontHappen);
            }
        }

        public Header[] getResponseHeaders() {
            return responseHeaders;
        }

        public Header[] getResponseFooters() {
            return responseFooters;
        }

        public Header getResponseHeader(String headerName) {
            for (Header header : responseHeaders) {
                if (header.getName().equalsIgnoreCase(headerName)) {
                    return header;
                }
            }
            return null;
        }
    }
}