001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.integration;
007
008import static com.gargoylesoftware.htmlunit.BrowserVersion.FIREFOX;
009import static com.google.common.collect.Lists.transform;
010import static java.util.UUID.randomUUID;
011import static javax.ws.rs.core.HttpHeaders.ACCEPT;
012import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
013import static javax.ws.rs.core.Response.Status.NOT_FOUND;
014import static org.fcrepo.kernel.api.FedoraTypes.FCR_METADATA;
015import static org.fcrepo.kernel.api.RdfLexicon.REPOSITORY_NAMESPACE;
016import static org.junit.Assert.assertEquals;
017import static org.junit.Assert.assertNotEquals;
018import static org.junit.Assert.assertNotNull;
019import static org.junit.Assert.assertTrue;
020import static org.junit.Assert.fail;
021
022import java.io.IOException;
023import java.nio.charset.StandardCharsets;
024import java.util.List;
025import java.util.concurrent.TimeUnit;
026
027import org.apache.http.HttpResponse;
028import org.apache.http.auth.AuthScope;
029import org.apache.http.client.CredentialsProvider;
030import org.apache.http.client.methods.HttpPatch;
031import org.apache.http.entity.StringEntity;
032import org.junit.After;
033import org.junit.Before;
034import org.junit.Test;
035
036import com.gargoylesoftware.htmlunit.DefaultCredentialsProvider;
037import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
038import com.gargoylesoftware.htmlunit.IncorrectnessListener;
039import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
040import com.gargoylesoftware.htmlunit.Page;
041import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
042import com.gargoylesoftware.htmlunit.WebClient;
043import com.gargoylesoftware.htmlunit.html.DomAttr;
044import com.gargoylesoftware.htmlunit.html.DomElement;
045import com.gargoylesoftware.htmlunit.html.DomText;
046import com.gargoylesoftware.htmlunit.html.HtmlButton;
047import com.gargoylesoftware.htmlunit.html.HtmlElement;
048import com.gargoylesoftware.htmlunit.html.HtmlFileInput;
049import com.gargoylesoftware.htmlunit.html.HtmlForm;
050import com.gargoylesoftware.htmlunit.html.HtmlInput;
051import com.gargoylesoftware.htmlunit.html.HtmlPage;
052import com.gargoylesoftware.htmlunit.html.HtmlSelect;
053import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
054
055/**
056 * <p>FedoraHtmlResponsesIT class.</p>
057 *
058 * @author cbeer
059 */
060public class FedoraHtmlResponsesIT extends AbstractResourceIT {
061
062    private WebClient webClient;
063    private WebClient javascriptlessWebClient;
064
065    @Before
066    public void setUp() {
067        webClient = getDefaultWebClient();
068
069        javascriptlessWebClient = getDefaultWebClient();
070        javascriptlessWebClient.getOptions().setJavaScriptEnabled(false);
071    }
072
073    @After
074    public void cleanUp() {
075        webClient.close();
076        javascriptlessWebClient.close();
077    }
078
079    @Test
080    public void testDescribeHtml() throws IOException {
081        final HtmlPage page = webClient.getPage(serverAddress);
082        ((HtmlElement)page.getFirstByXPath("//h4[text()='Properties']")).click();
083
084        checkForHeaderBranding(page);
085        final String namespaceLabel = page
086            .getFirstByXPath("//span[@title='" + REPOSITORY_NAMESPACE + "']/text()")
087            .toString();
088
089        assertEquals("Expected to find namespace URIs displayed as their prefixes", "fedora:",
090                        namespaceLabel);
091    }
092
093    @Test
094    public void testCreateNewNodeWithProvidedId() throws IOException {
095        createAndVerifyObjectWithIdFromRootPage(newPid());
096    }
097
098    private String newPid() {
099        return randomUUID().toString();
100    }
101
102    private HtmlPage createAndVerifyObjectWithIdFromRootPage(final String pid) throws IOException {
103        return createAndVerifyObjectWithIdFromRootPage(pid, "basic container");
104    }
105
106    private HtmlPage createAndVerifyObjectWithIdFromRootPage(final String pid, final String containerType)
107            throws IOException {
108        final HtmlPage page = webClient.getPage(serverAddress);
109        final HtmlForm form = (HtmlForm)page.getElementById("action_create");
110        final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin");
111        type.getOptionByValue(containerType).setSelected(true);
112
113        final HtmlInput new_id = (HtmlInput)page.getElementById("new_id");
114        new_id.setValueAttribute(pid);
115        final HtmlButton button = form.getFirstByXPath("button");
116        button.click();
117
118
119        try {
120            final HtmlPage page1 = webClient.getPage(serverAddress + pid);
121            assertEquals("Page had wrong title!", serverAddress + pid, page1.getTitleText());
122            return page1;
123        } catch (final FailingHttpStatusCodeException e) {
124            fail("Did not successfully retrieve created page! Got HTTP code: " + e.getStatusCode());
125            return null;
126        }
127    }
128
129    @Test
130    public void testCreateNewNodeWithGeneratedId() throws IOException {
131
132        final HtmlPage page = webClient.getPage(serverAddress);
133        final HtmlForm form = (HtmlForm)page.getElementById("action_create");
134        final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin");
135        type.getOptionByValue("basic container").setSelected(true);
136        final HtmlButton button = form.getFirstByXPath("button");
137        button.click();
138
139        final HtmlPage page1 = javascriptlessWebClient.getPage(serverAddress);
140        assertTrue("Didn't see new information in page!", !page1.asText().equals(page.asText()));
141    }
142
143    @Test
144    public void testCreateNewBasicContainer() throws IOException {
145        final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "basic container");
146        assertTrue("Set container type to ldp:BasicContainer", newPage.asText().contains(
147                "http://www.w3.org/ns/ldp#BasicContainer"));
148    }
149
150    @Test
151    public void testCreateNewDirectContainer() throws IOException {
152        final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "direct container");
153        assertTrue("Set container type to ldp:DirectContainer", newPage.asText().contains(
154                "http://www.w3.org/ns/ldp#DirectContainer"));
155    }
156
157    @Test
158    public void testCreateNewIndirectContainer() throws IOException {
159        final HtmlPage newPage = createAndVerifyObjectWithIdFromRootPage(newPid(), "indirect container");
160        assertTrue("Set container type to ldp:IndirectContainer", newPage.asText().contains(
161                "http://www.w3.org/ns/ldp#IndirectContainer"));
162    }
163
164    @Test
165    public void testCreateNewDatastream() throws Exception {
166
167        // can't do this with javascript, because HTMLUnit doesn't speak the HTML5 file api
168        final HtmlPage page = javascriptlessWebClient.getPage(serverAddress);
169
170        final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin");
171        type.getOptionByValue("binary").setSelected(true);
172
173        final HtmlFileInput fileInput = (HtmlFileInput)page.getElementById("binary_payload");
174        fileInput.setData("abcdef".getBytes());
175        fileInput.setContentType("application/pdf");
176
177        final HtmlButton button = (HtmlButton)page.getElementById("btn_action_create");
178        button.click();
179
180        // Without Javascript you end up at a blank page with just the newly generated URI as text.
181        final Page resultPage = javascriptlessWebClient.getCurrentWindow().getEnclosedPage();
182        final String newUri = resultPage.getWebResponse().getContentAsString();
183
184        final Page page1 = javascriptlessWebClient.getPage(newUri + "/" + FCR_METADATA);
185        assertTrue(page1.isHtmlPage());
186        assertEquals(newUri, ((HtmlPage)page1).getTitleText());
187    }
188
189    @Test
190    public void testCreateNewDatastreamWithNoFileAttached() throws IOException {
191
192        final String pid = newPid();
193
194        // can't do this with javascript, because HTMLUnit doesn't speak the HTML5 file api
195        final HtmlPage page = webClient.getPage(serverAddress);
196        final HtmlForm form = (HtmlForm)page.getElementById("action_create");
197
198        final HtmlInput slug = form.getInputByName("slug");
199        slug.setValueAttribute(pid);
200
201        final HtmlSelect type = (HtmlSelect)page.getElementById("new_mixin");
202        type.getOptionByValue("binary").setSelected(true);
203
204        final HtmlButton button = form.getFirstByXPath("button");
205        button.click();
206
207        javascriptlessWebClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
208        final int status = javascriptlessWebClient.getPage(serverAddress + pid).getWebResponse().getStatusCode();
209        assertEquals(NOT_FOUND.getStatusCode(), status);
210    }
211
212    @Test
213    public void testCreateNewObjectAndDeleteIt() throws IOException {
214        final boolean throwExceptionOnFailingStatusCode = webClient.getOptions().isThrowExceptionOnFailingStatusCode();
215        webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);
216
217        final String pid = createNewObject();
218        final HtmlPage page = webClient.getPage(serverAddress + pid);
219        final HtmlForm action_delete = page.getFormByName("action_delete");
220        action_delete.getButtonByName("delete-button").click();
221        webClient.waitForBackgroundJavaScript(1000);
222        webClient.waitForBackgroundJavaScriptStartingBefore(10000);
223
224        final Page page2 = webClient.getPage(serverAddress + pid);
225        assertEquals("Didn't get a 410!", 410, page2.getWebResponse()
226                .getStatusCode());
227
228        webClient.getOptions().setThrowExceptionOnFailingStatusCode(throwExceptionOnFailingStatusCode);
229    }
230
231    @Test
232    public void testVersionsListWorksWhenNoVersionsPresent() throws IOException {
233        final boolean throwExceptionOnFailingStatusCode = webClient.getOptions().isThrowExceptionOnFailingStatusCode();
234        webClient.getOptions().setThrowExceptionOnFailingStatusCode(true);
235
236        final String pid = createNewObject();
237        final HtmlPage page = webClient.getPage(serverAddress + pid);
238        final DomElement viewVersions = page.getElementById("view_versions");
239        final Page versionsPage = viewVersions.click();
240        assertEquals("Didn't get a 200!", 200, versionsPage.getWebResponse()
241                                                    .getStatusCode());
242        webClient.getOptions().setThrowExceptionOnFailingStatusCode(throwExceptionOnFailingStatusCode);
243    }
244
245    /**
246     * This test walks through the steps for creating an object, setting some
247     * metadata, creating a version, updating that metadata, viewing the
248     * version history to find that old version.
249     *
250     * @throws IOException exception thrown during this function
251     */
252    @Test
253    public void testVersionCreationAndNavigation() throws Exception {
254        final String pid = newPid();
255        createAndVerifyObjectWithIdFromRootPage(pid);
256
257        TimeUnit.SECONDS.sleep(1);
258
259        final String updateSparql = "PREFIX dc: <http://purl.org/dc/elements/1.1/>\n" +
260                "PREFIX fedora: <" + REPOSITORY_NAMESPACE + ">\n" +
261                "\n" +
262                "INSERT DATA { <> dc:title \"Object Title\". }";
263        postSparqlUpdateUsingHttpClient(updateSparql, pid);
264
265        final HtmlPage objectPage = javascriptlessWebClient.getPage(serverAddress + pid);
266        assertEquals("Title should be set.", "Object Title",
267                     objectPage.getFirstByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()")
268                             .toString());
269
270        TimeUnit.SECONDS.sleep(1);
271        final String updateSparql2 = "PREFIX dc: <http://purl.org/dc/elements/1.1/>\n" +
272                "\n" +
273                "DELETE { <> dc:title ?t }\n" +
274                "INSERT { <> dc:title \"Updated Title\" }" +
275                "WHERE { <> dc:title ?t }";
276        postSparqlUpdateUsingHttpClient(updateSparql2, pid);
277
278        final HtmlPage versions =
279                javascriptlessWebClient.getPage(serverAddress + pid + "/fcr:versions");
280        final List<DomAttr> versionLinks =
281            castList(versions.getByXPath("//a[@class='version_link']/@href"));
282        assertEquals("There should be three versions.", 3, versionLinks.size());
283
284        // get the labels
285        // will look like "Version from 2013-00-0T00:00:00.000Z"
286        // and will sort chronologically based on a String comparison
287        final List<DomText> labels =
288            castList(versions
289                    .getByXPath("//a[@class='version_link']/text()"));
290        final boolean chronological = labels.get(0).asText().compareTo(labels.get(1).toString()) < 0;
291        logger.debug("Versions {} in chronological order: {}, {}",
292                     chronological ? "are" : "are not", labels.get(0).asText(), labels.get(1).asText());
293
294        final HtmlPage firstRevision =
295                javascriptlessWebClient.getPage(versionLinks.get(chronological ? 1 : 2)
296                    .getNodeValue());
297        final List<DomText> v1Titles =
298            castList(firstRevision
299                    .getByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()"));
300        final HtmlPage secondRevision =
301                javascriptlessWebClient.getPage(versionLinks.get(chronological ? 2 : 1)
302                    .getNodeValue());
303        final List<DomText> v2Titles =
304            castList(secondRevision
305                    .getByXPath("//span[@property='http://purl.org/dc/elements/1.1/title']/text()"));
306
307        assertEquals("Version one should have one title.", 1, v1Titles.size());
308        assertEquals("Version two should have one title.", 1, v2Titles.size());
309        assertNotEquals("Each version should have a different title.", v1Titles.get(0), v2Titles.get(0));
310        assertEquals("First version should be preserved.", "Object Title", v1Titles.get(0).getWholeText());
311        assertEquals("Second version should be preserved.", "Updated Title", v2Titles.get(0).getWholeText());
312    }
313
314    private static void postSparqlUpdateUsingHttpClient(final String sparql, final String pid) throws IOException {
315        final HttpPatch method = new HttpPatch(serverAddress + pid);
316        method.addHeader(CONTENT_TYPE, "application/sparql-update");
317        final StringEntity entity = new StringEntity(sparql, StandardCharsets.UTF_8);
318        method.setEntity(entity);
319        final HttpResponse response = client.execute(method);
320        assertEquals("Expected successful response.", 204,
321                response.getStatusLine().getStatusCode());
322    }
323
324    @Test
325    public void testCreateNewObjectAndSetProperties() throws Exception {
326        final String pid = createNewObject();
327
328        final HtmlPage page = webClient.getPage(serverAddress + pid);
329        final HtmlForm form = (HtmlForm)page.getElementById("action_sparql_update");
330        final HtmlTextArea sparql_update_query = (HtmlTextArea)page.getElementById("sparql_update_query");
331        sparql_update_query.setText("INSERT { <> <info:some-predicate> 'asdf' } WHERE { }");
332
333        final HtmlButton button = form.getFirstByXPath("button");
334        button.click();
335
336        final HtmlPage page1 = webClient.getPage(serverAddress + pid);
337        assertTrue(page1.getElementById("metadata").asText().contains("some-predicate"));
338    }
339
340    @Test
341    public void testSimpleSearch() throws Exception {
342        final HtmlPage page = webClient.getPage(serverAddress);
343        page.getAnchorByText("Search").click();
344
345        final HtmlPage searchPage = (HtmlPage)webClient.getCurrentWindow().getEnclosedPage();
346
347        final HtmlForm form = (HtmlForm)searchPage.getElementById("action_search");
348        final HtmlInput q = (HtmlInput)searchPage.getElementById("search_value_1");
349        q.setValueAttribute("info:fedora/*");
350        final HtmlButton button = form.getFirstByXPath("button");
351        button.click();
352    }
353
354
355    private static void checkForHeaderBranding(final HtmlPage page) {
356        assertNotNull(
357                page.getFirstByXPath("//nav[@role='navigation']/div[@class='navbar-header']/a[@class='navbar-brand']"));
358    }
359
360    private String createNewObject() throws IOException {
361
362        final String pid = newPid();
363
364        final HtmlPage page = webClient.getPage(serverAddress);
365        final HtmlForm form = page.getFormByName("action_create");
366
367        final HtmlInput slug = form.getInputByName("slug");
368        slug.setValueAttribute(pid);
369
370        final HtmlButton button = form.getFirstByXPath("button");
371        button.click();
372
373        webClient.waitForBackgroundJavaScript(1000);
374        webClient.waitForBackgroundJavaScriptStartingBefore(10000);
375        return pid;
376    }
377
378    private CredentialsProvider getFedoraAdminCredentials() {
379        final CredentialsProvider credentials  = new DefaultCredentialsProvider();
380        credentials.setCredentials(AuthScope.ANY, FEDORA_ADMIN_CREDENTIALS);
381        return credentials;
382    }
383
384    private WebClient getDefaultWebClient() {
385
386        final WebClient webClient = new WebClient(FIREFOX);
387        webClient.addRequestHeader(ACCEPT, "text/html");
388        webClient.setCredentialsProvider(getFedoraAdminCredentials());
389        webClient.waitForBackgroundJavaScript(1000);
390        webClient.waitForBackgroundJavaScriptStartingBefore(10000);
391        webClient.setAjaxController(new NicelyResynchronizingAjaxController());
392        //Suppress warning from IncorrectnessListener
393        webClient.setIncorrectnessListener(new SuppressWarningIncorrectnessListener());
394        //Suppress css warning with the silent error handler.
395        webClient.setCssErrorHandler(new SilentCssErrorHandler());
396        return webClient;
397
398    }
399
400    @SuppressWarnings("unchecked")
401    private static <T> List<T> castList(final List<?> l) {
402        return transform(l, x -> (T) x);
403    }
404
405    private static class SuppressWarningIncorrectnessListener
406            implements IncorrectnessListener {
407        @Override
408        public void notify(final String arg0, final Object arg1) {
409
410        }
411    }
412
413}