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