content: /**
   * Copyright 2014 55 Minutes (http://www.55minutes.com)
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *    http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package fiftyfive.wicket.test;
  
  import java.io.IOException;
  import java.io.StringReader;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import javax.xml.parsers.DocumentBuilder;
  
  import org.w3c.dom.Document;
  import org.xml.sax.ErrorHandler;
  import org.xml.sax.InputSource;
  import org.xml.sax.SAXException;
  import org.xml.sax.SAXParseException;
  
  /**
   * Base class for XHTML and HTML5 validators.
   */
  public abstract class AbstractDocumentValidator implements ErrorHandler
  {
      static final int DEF_NUM_LINES_CONTEXT = 10;
      
      private int     numLinesContext = DEF_NUM_LINES_CONTEXT;
      private boolean parsed = false;
      private String[] lines = null;
      private List<String> errors = new ArrayList();
      private Document doc;
      
      /**
       * Sets the number of lines of context that will be listed along with
       * the line that has the validation error. Must be positive.
       * The default is 10.
       */
      public void setNumLinesContext(int newNumLinesContext)
      {
          if(newNumLinesContext < 0)
          {
              throw new IllegalArgumentException(
                  "numLinesContext cannot be zero or negative"
              );
          }
          this.numLinesContext = newNumLinesContext;
      }
      
      /**
       * Parses a document provided as a String. To test whether the
       * document was parsed successfully and is valid, subsequently call
       * {@link #isValid}.
       */
      public void parse(String document) throws IOException
      {
          if(this.parsed)
          {
              throw new IllegalStateException(
                  "parse() can only be used once. " +
                  "Create a new instance for each document you wish to parse."
              );
          }
          try
          {
              this.lines = document.split("\\r?\\n");
              this.doc = builder().parse(new InputSource(new StringReader(document)));
              this.parsed = true;
          }
          catch(SAXException saxe)
          {
              // ignore
          }
      }
      
      /**
       * Returns <code>true</code> if the previous call to {@link #parse}
       * completed without errors or warnings.
       */
      public boolean isValid()
      {
          return this.errors.size() == 0 && this.parsed;
      }
      
      public Document getDocument()
      {
          if(!this.parsed)
          {
              throw new IllegalStateException(
                  "A document does not exist because parse() has not yet been " +
                  "called. Parse first and then call getDocument()."
              );
          }
          return this.doc;
      }
      
      /**
       * Returns the errors and warnings that were encountered in the previous
       * call to {@link #parse}, if any.
       */
      public List<String> getErrors()
      {
          return Collections.unmodifiableList(this.errors);
      }
      
      
      // ==== ErrorHandler callbacks===========================================
      
      public void error(SAXParseException exception)
      {
          this.errors.add(format(exception));
      }
      
      public void fatalError(SAXParseException exception)
      {
          this.errors.add(format(exception));
      }
      
      public void warning(SAXParseException exception)
      {
          this.errors.add(format(exception));
      }
  
      // ==== End ErrorHandler callbacks ======================================
      
      /**
       * Construct a validating DocumentBuilder with <code>this</code>
       * registered as the error handler. This is the parsing and validation
       * strategy that will be used.
       * Subclasses should provide the appropriate XHTML or HTML5 implementation.
       */
      protected abstract DocumentBuilder builder();
      
      /**
       * Formats a SAXParseException as a string: error message, followed
       * by five lines of the markup centered around the line where the error
       * occurs.
       */
      private String format(SAXParseException ex)
      {
          StringBuffer buf = new StringBuffer();
          buf.append(ex.getMessage());
          buf.append("\n");
          
          final int ctx = this.numLinesContext;
          for(int offset = -1 * ctx/2; offset <= ctx/2; offset++)
          {
              int line = ex.getLineNumber() + offset;
              if(line >= 1 && line <= this.lines.length)
              {
                  buf.append("   ");
                  if(line == ex.getLineNumber())
                  {
                      buf.append("*");
                  }
                  else
                  {
                      buf.append(" ");
                  }
                  buf.append(String.format("%-5d", line));
                  buf.append("| ");
                  buf.append(this.lines[line-1]);
                  buf.append("\n");
              }
          }
          return buf.toString();
      }
  }
  