Die Klasse LexingValidator

Diese Klasse verwende ich zum Überprüfen gewisser grundlegender Eigenschaften des von BSP-Applikationen gerenderten HTML-Codes. Ich habe sie im Anhang des BSP-Praxisbuchs dokumentiert. Zum eigentlichen Durchlesen des HTML-Quelltextes verwende ich die Lexing-Komponente eines bei sourceforge.net frei verfügbaren HTML-Parsers. Da die Prüfungen nur sehr allgemeiner Art sind, reicht der Lexer hier völlig aus.

Die Texte der Fehlermeldungen habe ich in eine .properties Datei ausgelagert, ebenso gewisse Einstellungen. Texte und Einstellungen lassen sich also mit einem einfachen Text-Editor ändern und werden beim nächsten Programmstart wirksam, ohne dass der Java-Code neu generiert werden muss. Konkret handelt es sich neben Meldungstexten um die Festlegungen,

Die Ressource LexingValidator.properties muss im Klassenpfad am selben Ort stehen, an dem auch LexingValidator.class zu finden ist. Hier der vollständige Inhalt:
# Einstellungen für den Lexing Validator
# Werden gelesen von LexingValidator.java
E_NESTED_FORMS         = E: Geschachtelte <form>s nicht erlaubt
E_UNNAMED_INPUT        = E: <input>-Tag ohne Namen,
E_NONUNIQUE_ID         = E: Attribut id kommt mehrfach vor
E_EMPTY_STACK          = E: Es kann kein Tag geschlossen werden (Stapelunterlauf)
E_DOUBLE_TAG           = E: Tag darf nur einmal in der Seite vorkommen
E_BODY_IN_HEAD         = E: <body> wurde geöffnet, ohne <head> zu schliessen
E_HEAD_AFTER_BODY      = E: Nach <body> darf kein <head> mehr kommen
E_ILLEGAL_TAG_POS      = E: Ausserhalb von <head>...</head> und <body>...</body> dürfen keine Tags stehen
E_UNRESOLVED_STACK     = E: Stack konnte nicht abgebaut werden (Schachtelungsfehler)
uniqueTags             = !DOCTYPE, HTML, HEAD, BODY
nonClosingTags         = BR, P, INPUT, TEXTAREA, AREA, HR, IMG, LI, META, LINK, COL, !DOCTYPE, OPTION
skipTags               = SCRIPT, STYLE
Das Programm setzt einerseits das Paket gnu.getopt voraus (einen Java-Port der bekannten getopt Komponente, siehe http://www.urbanophile.com/arenn/hacking/download.html), andererseits natürlich die beiden .jar Dateien htmllexer.jar und htmlparser.jar des HTML-Parsers.

Das Programm unterstützt die folgenden Schalter in der Kommandozeile:

Interessant an diesem Parser ist übrigens, dass er auch Scripting-Tags (<%...%>) erkennt. Er sollte daher auch gut geeignet sein, um beispielsweise einen Pretty Printer für BSP-Views zu schreiben. Zu seinen Schwächen gehört dagegen, dass es sich um einen DOM-Parser handelt. Der Parser selbst hat darüberhinaus noch eine sehr starr festgelegte Elementstruktur, so dass man auch für einen BSP-View-Pretty-Printer wohl nur den Lexer verwenden kann (und nicht den Parser). Vielleicht wäre es daher besser, für ein solches Vorhaben mit Perl zu arbeiten und den SAX-Parser HTML::Parser zu verwenden.

Update am 31.7.2011: Mittlerweile gibt es den hier angekündigten Pretty Printer für BSP-Views auf Perl-Basis, und er ist bereits seit einigen Jahren in unserem BSP-Entwicklungsteam im Einsatz. Auf dieser Webseite findet sich unter "Ergänzungen" ein dokumentierender Artikel: Ein Pretty Printer für BSP-Views.

Ansicht erzeugt mit dem Generic Source Highlighter, © 2004 bei Nigel McNie
  1.  
  2. import org.htmlparser.lexer.Lexer;
  3. import org.htmlparser.Node;
  4. import org.htmlparser.Tag;
  5. import org.htmlparser.util.ParserException;
  6. import org.htmlparser.nodes.TagNode;
  7. import org.htmlparser.lexer.Page;
  8.  
  9. import gnu.getopt.Getopt;
  10.  
  11. import java.io.File;
  12. import java.io.Writer;
  13. import java.io.FileInputStream;
  14. import java.io.FileWriter;
  15. import java.io.StringWriter;
  16. import java.io.IOException;
  17.  
  18. import java.util.HashMap;
  19. import java.util.HashSet;
  20. import java.util.Properties;
  21. import java.util.Stack;
  22. import java.util.EmptyStackException;
  23.  
  24. public class LexingValidator {
  25.  
  26.   public static int traceLevel = 0;
  27.   public static Writer log;
  28.   private Lexer l;
  29.  
  30.   public static final String C_DUMMY = "1";
  31.   public static final String C_DONE = "DONE";
  32.   public static final String C_OPEN = "OPEN";
  33.  
  34.   public static HashSet nonClosingTags = new HashSet()
  35.   public static HashSet uniqueTags = new HashSet();
  36.   public static HashSet skipTags = new HashSet();
  37.   private static Properties props = new Properties();
  38.    
  39.   static {
  40.     String[] values;
  41.     int i = 0;
  42.     try {
  43. // Ressourcen und Customizing einlesen     
  44.       props.load( Class.forName("HTMLParser.LexingValidator").
  45.                         getResourceAsStream("LexingValidator.properties" ));
  46.       values = props.getProperty("nonClosingTags").split("\\s*,\\s*");
  47.       for (i = 0; i<values.length; i++) nonClosingTags.add(values[i]);
  48.       values = props.getProperty("uniqueTags").split("\\s*,\\s*");
  49.       for (i = 0; i<values.length; i++) uniqueTags.add(values[i]);
  50.       values = props.getProperty("skipTags").split("\\s*,\\s*");
  51.       for (i = 0; i<values.length; i++) skipTags.add(values[i]);
  52.     } catch (Exception e) { e.printStackTrace(); }
  53.   } 
  54.    
  55.  
  56.   public LexingValidator( String fileName ) throws IOException {
  57.     l = new Lexer(new Page(new FileInputStream(fileName), null));
  58.    
  59.     }
  60.    
  61.   public static void main(String[] argv) throws Exception {
  62.     String fileName = null, dirName = null, logName = null;
  63.     Getopt g = new Getopt("LexingValidator",argv,"f:d:t:l:");
  64.     int c;
  65.  
  66. // Kommandozeilen-Argumente einlesen
  67.     while ((c = g.getopt())!=-1) {
  68.       switch (c)
  69.         {
  70.           case 'f':
  71.             fileName = g.getOptarg();
  72.             break;
  73.           case 'd':
  74.             dirName = g.getOptarg();
  75.             break;
  76.           case 'l':
  77.             logName = g.getOptarg();
  78.             break;
  79.           case 't':
  80.             traceLevel = Integer.parseInt(g.getOptarg());
  81.             break;
  82.         }
  83.     }
  84.    
  85. // Log-Datei oder String aufbauen   
  86.     if (logName != null) log = new FileWriter( logName );
  87.     else log = new StringWriter();
  88.  
  89. // Tracelevel protokollieren   
  90.     log.write("Trace Level: " + traceLevel + "\r\n");
  91.  
  92. // HTML-Datei(en) analysieren
  93.     if (dirName != null) validateDir( new File( dirName ) );
  94.     else if (fileName != null) validateFile( fileName );
  95.    
  96. // Ggf. Ergebnis ausgeben
  97.     if (logName == null) System.out.println(log.toString());
  98.     else {
  99.       log.close();
  100.       System.out.println( "Analyse abgeschlossen" );
  101.       System.out.println( "Log-Datei: " + logName  );
  102.     }
  103.  
  104.   }
  105.  
  106.   public static void validateDir(File dir) throws Exception {
  107.     File[] files = dir.listFiles();
  108.     log.write( "\r\nPrüfe Verzeichnis "+dir.getPath()+"\r\n");
  109.     if (files != null) {
  110.       for (int i = 0; i < files.length; i++)
  111.         if (files[i].isDirectory()) validateDir( files[i] );
  112.         else if (files[i].getName().indexOf(".htm")>0) validateFile(files[i].getPath());
  113.       }
  114.   }
  115.  
  116.   public static void validateFile(String fileName) throws Exception {
  117.     LexingValidator lv = null;
  118.     log.write("\r\nPrüfe Datei "+fileName+
  119.     "\r\n=================================================================================\r\n");
  120.     lv = new LexingValidator( fileName );
  121.     lv.validate();
  122.   }
  123.  
  124.   public void validate() {
  125.     Node n = null;
  126.     TagNode tn = null, tn1 = null;
  127.     boolean inForm = false;
  128.     HashMap pool = new HashMap();
  129.     HashMap idPool = new HashMap();
  130.     Stack<TagNode> nodes = new Stack<TagNode>();
  131.     String tagName,id;
  132.     boolean isEndTag;
  133.     String waitForEnd = "";
  134.     boolean waiting = false;
  135.    
  136.        
  137.     try {
  138.        
  139.       while ((n = l.nextNode()) != null) {
  140.        
  141.         if (n instanceof TagNode) {
  142.           tn = (TagNode) n;
  143.           tagName = tn.getTagName();
  144.           isEndTag = tn.isEndTag();
  145.          
  146. // Im Tracemodus das Tag ausgeben           
  147.           if (traceLevel > 0)
  148.             log.write("::  <" + (isEndTag ? "/" : " ") + tagName + ">\r\n");
  149.  
  150. // Soll dieses Element übersprungen werden?
  151.           if (!isEndTag && skipTags.contains(tagName)) {
  152.             if (traceLevel > 0)
  153.               log.write( "Elementinhalt von " + tagName + " wird übersprungen\n");
  154.             waitForEnd=tagName;
  155.             waiting = true;
  156.             continue;
  157.             }                   
  158.          
  159. // Wait for End?
  160.           if (waiting)
  161.             if (waitForEnd.equals(tagName)&&isEndTag) {
  162.               if (traceLevel > 0)
  163.                 log.write("Elementende von " + tagName + " erreicht\n");
  164.               waiting = false;
  165.               continue;
  166.               }
  167.             else
  168.               continue;         
  169.          
  170. // Einmalige Tags prüfen
  171.           if (uniqueTags.contains(tagName)) {
  172. // Keines dieser Tags darf zweimal vorkommen           
  173.             if (!isEndTag && pool.get(tagName)!=null) addCodedMessage("E_DOUBLE_TAG",n);
  174.             pool.put(tagName, isEndTag ? C_DONE : C_OPEN );
  175. // body darf nicht in head vorkommen           
  176.             if (tagName.equals("BODY") && (pool.get("HEAD").equals(C_OPEN)))
  177.               addCodedMessage("E_BODY_IN_HEAD",n);
  178. // nach body darf nicht noch einma head vorkommen
  179.             if (tagName.equals("HEAD") && (pool.get("BODY")!=null))
  180.               addCodedMessage("E_HEAD_AFTER_BODY",n);
  181.             }
  182.            
  183. // Keine Tags ausserhalb von <head>...</head> und <body>...</body>
  184.             if ( !(tagName.equals("HTML") ||
  185.                    tagName.equals("HEAD") ||
  186.                    tagName.equals("!DOCTYPE") ||
  187.                    tagName.equals("BODY") ) &&
  188.                ( (pool.get("HEAD")==null) || !pool.get("HEAD").equals(C_OPEN) ) &&
  189.                ( (pool.get("BODY")==null) || !pool.get("BODY").equals(C_OPEN) ) )
  190.                  addCodedMessage( "E_ILLEGAL_TAG_POS", n );
  191.            
  192. // ID vorhanden? Prüfen, ob mehrfach vergeben
  193.         if (((id=tn.getAttribute("id"))!=null)&&(id.length() > 0)) {
  194.           if (idPool.get(id)!=null) addCodedMessage("E_NONUNIQUE_ID",n);
  195.           idPool.put(id,tn);
  196.         }
  197.  
  198.  
  199. // Es darf nur ein Formular offen sein
  200.           if (tagName.equals("FORM"))
  201.             if (!isEndTag) {
  202.               if (inForm) addCodedMessage("E_NESTED_FORMS",n);
  203.               inForm = true;
  204.               }
  205.             else inForm = false;
  206.            
  207. // Stack pflegen         
  208.           if (!nonClosingTags.contains(tagName)) {
  209.             if (!isEndTag)
  210.               nodes.push(tn);
  211.             else
  212.               try {
  213.                 tn1 = (TagNode) nodes.pop();
  214.                 if (!tn1.getTagName().equals(tagName))
  215.                   addMessage( "E: Tag <"+tn1.getTagName()+"> soll durch </"
  216.                                         +tagName+"> geschlossen werden",n);
  217.                 continue;
  218.                 } catch (EmptyStackException e1) {
  219.                    addCodedMessage("E_EMPTY_STACK",n);
  220.                    }
  221.                 }               
  222.             }
  223.         }
  224.        
  225.        
  226. // Ist hinterher noch etwas im Stack offen? Ausgeben, Fehler
  227.         if (nodes.size() > 0) {
  228.           addCodedMessage("E_UNRESOLVED_STACK",null);
  229.           tagName="";
  230.           while (!nodes.empty()) tagName += "" + nodes.size() + ": " + nodes.pop().getTagName() + "\n";
  231.           addMessage(tagName,null);
  232.           }
  233.        
  234.        
  235.         addMessage( "...fertig", null );
  236.      } catch (Exception ex) { ex.printStackTrace(); }         
  237.    }
  238.            
  239.            
  240.   private void addCodedMessage(String code, Node n) {
  241.     String msg = props.getProperty(code);
  242.     if (msg == null) msg = code;
  243.     addMessage( msg, n );
  244.     }
  245.  
  246.   private void addMessage(String msg, Node n) {
  247.     try {
  248.       if (n != null)
  249.         if (n instanceof Tag)
  250.           log.write("Zeile "+(((Tag)n).getStartingLineNumber()+1)+": <" + n.getText() + ">\r\n");
  251.         else
  252.           log.write(n+"\r\n");
  253.       log.write("  " + msg+"\r\n");
  254.       } catch (Exception ex)
  255.           { ex.printStackTrace(); }   
  256.    }
  257.  
  258. }


Quelltext herunterladen
 
Zurück zur Code Area
Powered by GeSHi