Coverage Report - org.phandom.Phandom
 
Classes in this File Line Coverage Branch Coverage Complexity
Phandom
16%
6/37
0%
0/12
1.9
Phandom$AjcClosure1
100%
1/1
N/A
1.9
Phandom$AjcClosure3
0%
0/1
N/A
1.9
Phandom$AjcClosure5
0%
0/1
N/A
1.9
 
 1  12
 /**
 2  
  * Copyright (c) 2013-2014, phandom.org
 3  
  * All rights reserved.
 4  
  *
 5  
  * Redistribution and use in source and binary forms, with or without
 6  
  * modification, are permitted provided that the following conditions
 7  
  * are met: 1) Redistributions of source code must retain the above
 8  
  * copyright notice, this list of conditions and the following
 9  
  * disclaimer. 2) Redistributions in binary form must reproduce the above
 10  
  * copyright notice, this list of conditions and the following
 11  
  * disclaimer in the documentation and/or other materials provided
 12  
  * with the distribution. 3) Neither the name of the phandom.org nor
 13  
  * the names of its contributors may be used to endorse or promote
 14  
  * products derived from this software without specific prior written
 15  
  * permission.
 16  
  *
 17  
  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 18  
  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
 19  
  * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 20  
  * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 21  
  * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 22  
  * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23  
  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 24  
  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 25  
  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 26  
  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 27  
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 28  
  * OF THE POSSIBILITY OF SUCH DAMAGE.
 29  
  */
 30  
 package org.phandom;
 31  
 
 32  
 import com.jcabi.aspects.Immutable;
 33  
 import com.jcabi.aspects.Loggable;
 34  
 import com.jcabi.log.Logger;
 35  
 import com.jcabi.log.VerboseProcess;
 36  
 import java.io.File;
 37  
 import java.io.IOException;
 38  
 import java.io.InputStream;
 39  
 import java.net.URI;
 40  
 import java.net.URL;
 41  
 import java.util.logging.Level;
 42  
 import java.util.regex.Pattern;
 43  
 import javax.validation.constraints.NotNull;
 44  
 import javax.xml.parsers.DocumentBuilderFactory;
 45  
 import javax.xml.parsers.ParserConfigurationException;
 46  
 import lombok.EqualsAndHashCode;
 47  
 import lombok.ToString;
 48  
 import org.apache.commons.io.IOUtils;
 49  
 import org.apache.commons.lang3.CharEncoding;
 50  
 import org.w3c.dom.Document;
 51  
 import org.xml.sax.SAXException;
 52  
 
 53  
 /**
 54  
  * PhantomJS DOM.
 55  
  *
 56  
  * <p>Use it to parse XML/XHTML/HTML document using PhantomJS, for example:
 57  
  *
 58  
  * <pre>Document dom = new Phandom(
 59  
  *   "&lt;html&gt;&lt;p&gt;Hey!&lt;/p&gt;&lt;/html&gt;"
 60  
  * ).dom();
 61  
  * Element element = dom.getElementByTag("p");</pre>
 62  
  *
 63  
  * <p>The most popular use case for the class would be its usage
 64  
  * in a unit test, to make sure your HTML document (together with its
 65  
  * embedded JavaScript scripts) is renderable by a browser:
 66  
  *
 67  
  * <pre>import com.rexsl.test.XhtmlMatchers;
 68  
  * import org.hamcrest.MatcherAssert;
 69  
  * import org.junit.Assume;
 70  
  * import org.junit.Test;
 71  
  * import org.phandom.Phandom;
 72  
  * public class HtmlTest {
 73  
  *   &#64;Test
 74  
  *   public void testPageRenderability() {
 75  
  *     Assume.assumeTrue(Phandom.isInstalled());
 76  
  *     MatcherAssert.assertThat(
 77  
  *       new Phandom("&lt;html>&lt;p>Hey!&lt;/p>&lt;/html>").dom(),
 78  
  *       XhtmlMatchers.hasXPath("//body/p[.='Hey!']")
 79  
  *     );
 80  
  *   }
 81  
  * }</pre>
 82  
  *
 83  
  * @author Yegor Bugayenko (yegor@tpc2.com)
 84  
  * @version $Id$
 85  
  * @since 0.1
 86  
  */
 87  
 @Immutable
 88  0
 @ToString
 89  0
 @EqualsAndHashCode(of = "page")
 90  
 @Loggable(Loggable.DEBUG)
 91  
 public final class Phandom {
 92  
 
 93  
     /**
 94  
      * Name of binary phantomjs.
 95  
      */
 96  
     private static final String BIN = "phantomjs";
 97  
 
 98  
     /**
 99  
      * Pattern to match phantomjs version.
 100  
      */
 101  1
     private static final Pattern VERSION =
 102  
         Pattern.compile("\\d+\\.\\d+\\.\\d+");
 103  
 
 104  
     /**
 105  
      * The page to render.
 106  
      */
 107  
     private final transient Page page;
 108  
 
 109  
     /**
 110  
      * Public ctor.
 111  
      * @param content Content to encapsulate
 112  
      */
 113  
     public Phandom(@NotNull final String content) {
 114  0
         this(new Page.Text(content));
 115  0
     }
 116  
 
 117  
     /**
 118  
      * Public ctor.
 119  
      * @param stream Stream with content
 120  
      * @throws IOException If fails to read the stream
 121  
      */
 122  
     public Phandom(@NotNull final InputStream stream) throws IOException {
 123  0
         this(IOUtils.toString(stream, CharEncoding.UTF_8));
 124  0
     }
 125  
 
 126  
     /**
 127  
      * Public ctor.
 128  
      * @param uri URI to render
 129  
      * @throws IOException If fails to read the stream
 130  
      * @since 0.3
 131  
      */
 132  
     public Phandom(@NotNull final URI uri) throws IOException {
 133  0
         this(new Page.Web(uri));
 134  0
     }
 135  
 
 136  
     /**
 137  
      * Public ctor.
 138  
      * @param url URL to render
 139  
      * @throws IOException If fails to read the stream
 140  
      * @since 0.3
 141  
      */
 142  
     public Phandom(@NotNull final URL url) throws IOException {
 143  0
         this(URI.create(url.toString()));
 144  0
     }
 145  
 
 146  
     /**
 147  
      * Public ctor.
 148  
      * @param file File to render
 149  
      * @throws IOException If fails to read the stream
 150  
      * @since 0.3
 151  
      */
 152  
     public Phandom(@NotNull final File file) throws IOException {
 153  0
         this(file.toURI());
 154  0
     }
 155  
 
 156  
     /**
 157  
      * Public ctor.
 158  
      * @param src Page with sources
 159  
      * @since 0.3
 160  
      */
 161  0
     private Phandom(final Page src) {
 162  0
         this.page = src;
 163  0
     }
 164  
 
 165  
     /**
 166  
      * PhantomJS binary is installed?
 167  
      * @return TRUE if installed
 168  
      * @since 0.2
 169  
      */
 170  
     public static boolean isInstalled() {
 171  
         String stdout;
 172  
         try {
 173  12
             stdout = new VerboseProcess(
 174  
                 new ProcessBuilder(Phandom.BIN, "--version"),
 175  
                 Level.FINE, Level.FINE
 176  
             ).stdoutQuietly().trim();
 177  6
         } catch (final IllegalStateException ex) {
 178  6
             stdout = ex.getLocalizedMessage();
 179  0
         }
 180  6
         return Phandom.VERSION.matcher(stdout).matches();
 181  
     }
 182  
 
 183  
     /**
 184  
      * Get DOM.
 185  
      * @return DOM
 186  
      * @throws IOException If fails
 187  
      */
 188  
     public Document dom() throws IOException {
 189  0
         final Process process = this.builder().start();
 190  0
         process.getOutputStream().close();
 191  0
         return Phandom.parse(
 192  
             new VerboseProcess(process, Level.FINE, Level.FINE).stdout()
 193  
         );
 194  
     }
 195  
 
 196  
     /**
 197  
      * Create process builder.
 198  
      * @return Builder
 199  
      * @throws IOException If fails
 200  
      */
 201  
     public ProcessBuilder builder() throws IOException {
 202  0
         final InputStream src = this.getClass().getResourceAsStream("dom.js");
 203  
         try {
 204  0
             return new ProcessBuilder(
 205  
                 Phandom.BIN,
 206  
                 new Temp(src, ".js").file().getAbsolutePath(),
 207  
                 this.page.uri().toString()
 208  
             );
 209  
         } finally {
 210  0
             src.close();
 211  
         }
 212  
     }
 213  
 
 214  
     /**
 215  
      * Parse XML into DOM.
 216  
      * @param xml XML to parse
 217  
      * @return DOM
 218  
      * @throws IOException If fails
 219  
      */
 220  
     private static Document parse(final String xml) throws IOException {
 221  0
         if (xml.isEmpty()) {
 222  0
             throw new IOException(
 223  
                 // @checkstyle LineLength (1 line)
 224  
                 "phantomjs produced an empty output instead of HTML, looks like an internal bug of phandom"
 225  
             );
 226  
         }
 227  
         try {
 228  0
             return DocumentBuilderFactory.newInstance()
 229  
                 .newDocumentBuilder()
 230  
                 .parse(IOUtils.toInputStream(xml, CharEncoding.UTF_8));
 231  0
         } catch (final ParserConfigurationException ex) {
 232  0
             Logger.warn(
 233  
                 Phandom.class,
 234  
                 "XML parsing failure on HTML by phantomjs:\n%s", xml
 235  
             );
 236  0
             throw new IOException("internal parsing error of phandom", ex);
 237  0
         } catch (final SAXException ex) {
 238  0
             Logger.warn(
 239  
                 Phandom.class,
 240  
                 "SAX failure on HTML by phantomjs:\n%s", xml
 241  
             );
 242  0
             throw new IOException("internal SAX error of phandom", ex);
 243  
         }
 244  
     }
 245  
 
 246  
 }