/* 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 org.fcrepo.test.api; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import static org.apache.http.HttpStatus.SC_NOT_FOUND; import static org.apache.http.HttpStatus.SC_OK; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.Charset; import java.util.Set; import junit.framework.JUnit4TestAdapter; import org.apache.commons.io.IOUtils; import org.apache.http.Header; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.FileBody; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.util.EntityUtils; import org.fcrepo.client.FedoraClient; import org.fcrepo.common.http.PreemptiveAuth; import org.fcrepo.server.access.FedoraAPIAMTOM; import org.fcrepo.server.management.FedoraAPIMMTOM; import org.fcrepo.server.types.gen.Datastream; import org.fcrepo.server.utilities.TypeUtility; import org.fcrepo.test.FedoraServerTestCase; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.JUnitCore; /** * 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 * @version $Id$ * @since 3.0 */ public class TestAdminAPI extends FedoraServerTestCase { private static FedoraClient s_client; private FedoraAPIAMTOM apia; private FedoraAPIMMTOM apim; // used for determining test configuration private static String authAccessProperty = "fedora.authorize.access"; protected Boolean authorizeAccess = null; protected String url; private final boolean chunked = false; @BeforeClass public static void bootstrap() throws Exception { s_client = getFedoraClient(); ingestSimpleImageDemoObjects(s_client); ingestSimpleDocumentDemoObjects(s_client); } @AfterClass public static void cleanUp() throws Exception { purgeDemoObjects(s_client); s_client.shutdown(); } @Before public void setUp() throws Exception { apia = s_client.getAPIAMTOM(); apim = s_client.getAPIMMTOM(); } @After public void tearDown() throws Exception { } // determine if test config specifies that Access requests should be authorized private boolean getAuthAccess() { if (authorizeAccess == null) { String property = System.getProperty(authAccessProperty); if (property.equals("true")) { authorizeAccess = true; } else if (property.equals("false")) { authorizeAccess = false; } else { assertTrue("Failed to determine whether to perform authorization on Access requests from: " + authAccessProperty, false); throw new RuntimeException( "Failed to determine whether to perform authorization on Access requests from: " + authAccessProperty); } } return authorizeAccess; } @Test public void testModifyControlGroup() throws Exception { ////////////////////////////////////////////////// // tests on file of PIDs - XML ////////////////////////////////////////////////// // get objects to modify from a basic search query, to provide xml input url = "/objects?pid=true&title=false&query=pid~demo%3A*&maxResults=1000&resultFormat=xml"; HttpResponse res = get(getAuthAccess()); assertEquals(SC_OK, res.getStatusLine().getStatusCode()); File objectsListFile = null; try { // output to a file objectsListFile = File.createTempFile("TestAdminAPI", null); FileWriter fw = new FileWriter(objectsListFile); String queryResults = EntityUtils.toString(res.getEntity()); fw.write(queryResults); fw.close(); // run the tests testFileInput(objectsListFile); } finally { // clean up if (objectsListFile != null) if (!objectsListFile.delete()) objectsListFile.deleteOnExit(); } purgeDemoObjects(s_client); ingestSimpleImageDemoObjects(s_client); ingestSimpleDocumentDemoObjects(s_client); ////////////////////////////////////////////////// // tests on file of PIDs - flat file ////////////////////////////////////////////////// objectsListFile = null; try { // generate flat file list of demo objects objectsListFile = File.createTempFile("TestAdminAPI", null); FileWriter fw = new FileWriter(objectsListFile); Set<String> objects = getDemoObjects(s_client); for (String pid : objects) { fw.write(pid + "\n"); } fw.close(); // run the tests testFileInput(objectsListFile); } finally { // clean up if (objectsListFile != null) if (!objectsListFile.delete()) objectsListFile.deleteOnExit(); } purgeDemoObjects(s_client); ingestSimpleImageDemoObjects(s_client); ingestSimpleDocumentDemoObjects(s_client); ////////////////////////////////////////////////// // tests on single object ////////////////////////////////////////////////// Set<String> objects = getDemoObjects(s_client); // test on the first object we find String pid = objects.toArray(new String[0])[0]; // test 404 on object not found url = this.modifyDatastreamControlGroupUrl(pid + "doesnotexist", "DC", "M", false, false, false); res = get(true); assertEquals(SC_NOT_FOUND, res.getStatusLine().getStatusCode()); // test 404 on datastream not found url = this.modifyDatastreamControlGroupUrl(pid, "doesnotexist", "M", false, false, false); res = get(true); assertEquals(SC_NOT_FOUND, res.getStatusLine().getStatusCode()); // TODO: getting stream contents? (could use REST call instead) // datastream contents before modification byte[] before = TypeUtility.convertDataHandlerToBytes(apia.getDatastreamDissemination(pid, "DC", null).getStream()); url = this.modifyDatastreamControlGroupUrl(pid, "DC", "M", false, false, false); res = get(true); String contents = EntityUtils.toString(res.getEntity()); assertEquals(SC_OK, res.getStatusLine().getStatusCode()); // check control group modified Datastream ds = apim.getDatastream(pid, "DC", null); assertEquals("ControlGroup", "M", ds.getControlGroup().value()); // datastream contents after modification byte[] after = TypeUtility.convertDataHandlerToBytes(apia.getDatastreamDissemination(pid, "DC", null).getStream()); // check they are the same // (comparing as strings as the assertEquals is a lot easier to read...) String beforeString = new String(before, "UTF-8"); String afterString = new String(after, "UTF-8"); assertEquals("Datastream contents ", beforeString, afterString); ////////////////////////////////////////////////// // tests on list of pids ////////////////////////////////////////////////// // test on the second and third objects we found String pid1 = objects.toArray(new String[0])[1]; String pid2 = objects.toArray(new String[0])[2]; // modify both url = this.modifyDatastreamControlGroupUrl(pid1 + "," + pid2, "DC", "M", false, false, false); res = get(true); assertEquals(SC_OK, res.getStatusLine().getStatusCode()); contents = EntityUtils.toString(res.getEntity()); int[] counts = getCounts(contents); // check for modification in result stream assertEquals("Object count", 2, counts[0]); assertEquals("Datastream count", 2, counts[1]); // check control groups actually modified ds = apim.getDatastream(pid1, "DC", null); assertEquals("ControlGroup", "M", ds.getControlGroup().value()); ds = apim.getDatastream(pid2, "DC", null); assertEquals("ControlGroup", "M", ds.getControlGroup().value()); } private void testFileInput(File objectsListFile) throws Exception { // count objects we know already have Managed content DC // (ingest creates some of these, ending in _M) // used later in checking results Set<String> objects = getDemoObjects(s_client); int managedObjects = 0; for (String pid : objects) { if (pid.endsWith("_M")) managedObjects++; } // modify datastreams, based on file input - DC url = this.modifyDatastreamControlGroupUrl("file:///" + objectsListFile.getAbsolutePath(), "DC", "M", false, false, false); HttpResponse res = get(true); int status = res.getStatusLine().getStatusCode(); String modified = EntityUtils.toString(res.getEntity()); assertEquals(SC_OK, status); // object and datastream count message expected String logExpected = "Updated " + (objects.size() - managedObjects) + " objects and " + (objects.size() - managedObjects) + " datastreams"; assertTrue("Wrong number of objects/datastreams updated. Expected " + logExpected + "\n" + "Log file shows:" + "\n" + modified, modified.contains(logExpected)); // do again, this time we expect no modifications (already modified, so should be ignored) res = get(true); modified = EntityUtils.toString(res.getEntity()); assertEquals(SC_OK, res.getStatusLine().getStatusCode()); // object and datastream count message expected logExpected = "Updated " + 0 + " objects and " + 0 + " datastreams"; assertTrue("Wrong number of objects/datastreams updated", modified.contains(logExpected)); // do again modifying DC and RELS-EXT // DC is already M, so won't result in modifications // not all objects have RELS-EXT- the simple document demo objects don't // so we check that object and datastream count is greater than zero but 2 less than number of objects // FIXME: could iterate all objects before/after and do more exact tests url = this.modifyDatastreamControlGroupUrl("file:///" + objectsListFile.getAbsolutePath(), "DC,RELS-EXT", "M", false, false, false); res = get(true); modified = EntityUtils.toString(res.getEntity()); assertEquals(SC_OK, res.getStatusLine().getStatusCode()); int[] counts = getCounts(modified); int objectCount = counts[0]; int datastreamCount = counts[1]; if (objectCount <= 0 || objectCount > (objects.size() - managedObjects - 2) || datastreamCount <= 0 || datastreamCount > (objects.size() - managedObjects - 2)) fail("Incorrect number of objects and datastreams modified: objects " + objectCount + " datastreams " + datastreamCount); } /** * Parses results, returns object count, datastream count * @param response * @return */ private int[] getCounts(String response) { int objectCountPos = response.lastIndexOf("Updated "); int datastreamCountPos = response.lastIndexOf(" objects and "); int datastreamEndCountPos = response.lastIndexOf(" datastreams"); int objectCount = Integer.parseInt(response.substring(objectCountPos + "Updated ".length(), datastreamCountPos)); int datastreamCount = Integer.parseInt(response.substring(datastreamCountPos + " objects and ".length(), datastreamEndCountPos)); int[] res = {objectCount, datastreamCount}; return res; } private String modifyDatastreamControlGroupUrl(String pid, String dsID, String controlGroup, boolean addXMLHeader, boolean reformat, boolean setMIMETypeCharset) { String ret; try { ret = "/management/control" + "?action=modifyDatastreamControlGroup" + "&pid=" + URLEncoder.encode(pid, "UTF-8") + "&dsID=" + URLEncoder.encode(dsID, "UTF-8") + "&controlGroup=" + controlGroup + "&addXMLHeader=" + addXMLHeader + "&reformat=" + reformat + "&setMIMETypeCharset=" + setMIMETypeCharset; } catch (UnsupportedEncodingException e) { // should never happen throw new RuntimeException(e); } return ret; } // helper methods private HttpClient getClient(boolean auth) { DefaultHttpClient client = new PreemptiveAuth(); if (auth) { client .getCredentialsProvider() .setCredentials(new AuthScope(getHost(), Integer .valueOf(getPort())), new UsernamePasswordCredentials(getUsername(), getPassword())); } return client; } /** * Issues an HTTP GET for the specified URL. * * @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 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; } HttpUriRequest httpMethod = null; if (method.equals("GET")) { httpMethod = new HttpGet(url); } else if (method.equals("DELETE")) { httpMethod = new HttpDelete(url); } else { throw new IllegalArgumentException("method must be one of GET or DELETE."); } httpMethod.setHeader(HttpHeaders.CONNECTION, "Keep-Alive"); return getClient(authenticate).execute(httpMethod); } 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; } HttpEntityEnclosingRequestBase httpMethod = null; try { if (method.equals("PUT")) { httpMethod = new HttpPut(url); } else if (method.equals("POST")) { httpMethod = new HttpPost(url); } else { throw new IllegalArgumentException("method must be one of PUT or POST."); } httpMethod.setHeader(HttpHeaders.CONNECTION, "Keep-Alive"); if (requestContent != null) { if (requestContent instanceof String) { StringEntity entity = new StringEntity((String) requestContent, Charset.forName("UTF-8")); entity.setChunked(chunked); httpMethod.setEntity(entity); } else if (requestContent instanceof File) { MultipartEntity entity = new MultipartEntity(); entity.addPart(((File) requestContent) .getName(), new FileBody((File) requestContent)); entity.addPart("param_name", new StringBody("value")); httpMethod.setEntity(entity); } else { throw new IllegalArgumentException("requestContent must be a String or File"); } } return getClient(authenticate).execute(httpMethod); } finally { if (httpMethod != null) { httpMethod.releaseConnection(); } } } class HttpResponseInfo { private final int statusCode; private final byte[] responseBody; private final Header[] responseHeaders; HttpResponseInfo(int status, byte[] body, Header[] headers) { statusCode = status; responseBody = body; responseHeaders = headers; } HttpResponseInfo(HttpResponse response) throws IOException { statusCode = response.getStatusLine().getStatusCode(); //responseBody = method.getResponseBody(); InputStream is = response.getEntity().getContent(); responseBody = IOUtils.toByteArray(is); IOUtils.closeQuietly(is); responseHeaders = response.getAllHeaders(); } 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 getResponseHeader(String headerName) { for (Header header : responseHeaders) { if (header.getName().equalsIgnoreCase(headerName)) { return header; } } return null; } } public static junit.framework.Test suite() { return new JUnit4TestAdapter(TestAdminAPI.class); } public static void main(String[] args) { JUnitCore.runClasses(TestAdminAPI.class); } }