/* 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; } } }