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