Java tutorial
/* Copyright (C) 2008 Helge Hess <helge.hess@opengroupware.org> This file is part of Go. Go is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. Go is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Go; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.getobjects.ofs.htaccess; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.Reader; import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * HtConfigParser * <p> * Read an NCSA configuration file into memory. It does not execute the * directives in any way. * <p> * http://httpd.apache.org/docs/2.2/configuring.html * <ul> * <li>one directive per line * <li>backslash as the last char to continue on the next line * <li>lines starting with '<' start a compound directive * <li>lines starting with '#' are comments * <li>comments may <b>not</b> be included after directives * <li>directives are case-insensitive * <li>arguments are often case-sensitive * <li>arguments are split by whitespace * <li>arguments can be quoted using '"' * </ul> */ public class HtConfigParser { protected static final Log log = LogFactory.getLog("htaccess"); protected LineNumberReader reader; protected Exception lastException; protected HtConfigFile file; protected ArrayList<IHtConfigContainer> parseStack; protected IHtConfigContainer currentContainer; protected boolean skipEmptyLines; protected boolean skipComments; public HtConfigParser(LineNumberReader _reader) { this.reader = _reader; this.file = new HtConfigFile(); this.parseStack = new ArrayList<IHtConfigContainer>(8); this.currentContainer = this.file; this.skipComments = true; this.skipEmptyLines = true; } public HtConfigParser(Reader _reader) { this(new LineNumberReader(_reader)); } public HtConfigParser(InputStream _is) { this(new InputStreamReader(_is)); } /* accessors */ public Exception lastException() { return this.lastException; } /** * Returns the HtConfigFile object which contains the results of the parsing * process. * If the parsing failed this method returns null and the error can be * retrieved using the lastException() method. * * @return an HtConfigFile object or null if the parsing failed. */ public HtConfigFile parsedFile() { return this.file; } /* trigger parsing */ public void parse() { String line; while ((line = this.readUnfoldedLine()) != null) { final int idx = this.findFirstNonWhitespace(line); /* empty lines */ if (idx == -1) { if (this.skipEmptyLines) continue; this.currentContainer.addNode(new HtConfigEmptyLine(line)); continue; } final char c0 = line.charAt(idx); /* comments */ if (c0 == '#') { if (this.skipComments) continue; this.currentContainer.addNode(new HtConfigComment(line)); continue; } /* close tags */ if (c0 == '<' && line.charAt(idx + 1) == '/') { /* tag close */ // TBD: check name match // TBD: check nesting depth int stackSize = this.parseStack != null ? this.parseStack.size() : 0; this.currentContainer = (stackSize > 0) ? this.parseStack.remove(stackSize - 1) : this.file; continue; } /* directive */ final List<String> parts = this.parseDirective(line.toCharArray(), idx, c0 == '<' ? '>' : 0); if (parts == null || parts.size() == 0) { log.error( "Failed to parse a HtConfigDirective, got no parts, line: " + this.reader.getLineNumber()); continue; } String[] args = (parts.size() > 1) ? parts.subList(1, parts.size()).toArray(emptyArgs) : emptyArgs; if (c0 == '<') { /* maintain compound directives, eg: <Files ~ "^([\d\]+)"> */ HtConfigSection section = new HtConfigSection(parts.get(0), args); this.currentContainer.addNode(section); this.parseStack.add(this.currentContainer); this.currentContainer = section; } else { /* plain directive */ HtConfigDirective plaindir = new HtConfigDirective(parts.get(0), args); this.currentContainer.addNode(plaindir); } } } private static final String[] emptyArgs = new String[0]; public List<String> parseDirective(final char[] _line, int _cursor, final char _extraStopChar) { List<String> parts = new ArrayList<String>(8); while ((_cursor = skipSpaces(_line, _cursor)) < _line.length) { /* here we are at a non-space char */ if (_line[_cursor] == _extraStopChar) { /* we are done */ break; } String s; int endIdx; if (_line[_cursor] == '"') { /* quoted token */ endIdx = this.findQuotedTokenEnd(_line, _cursor + 1); if (endIdx != -1) { /* found the closing quote */ // TBD: check for correctness (length) s = new String(_line, _cursor + 1, endIdx - _cursor - 1); parts.add(s); _cursor = endIdx + 1; /* skip closing quote */ continue; } /* if we didn't find a closing quote, we treat it like a regular line */ // intended fallthrough } /* non-quoted token */ endIdx = this.findTokenEnd(_line, _cursor, _extraStopChar); s = new String(_line, _cursor, endIdx - _cursor); parts.add(s); /* endIdx could pointer to WS, extraStop, or line end */ _cursor = endIdx; } return parts.size() > 0 ? parts : null; } private int skipSpaces(final char[] _line, int _cursor) { for (; _cursor < _line.length; _cursor++) { if (!Character.isWhitespace(_line[_cursor])) break; } return _cursor; } private int findTokenEnd(final char[] _line, int _cursor, final char _extraStopChar) { for (; _cursor < _line.length; _cursor++) { final char c0 = _line[_cursor]; if (c0 == _extraStopChar /* eg '>' */) break; if (Character.isWhitespace(c0)) /* found whitespace */ break; } return _cursor; } private int findQuotedTokenEnd(final char[] _line, int _cursor) { for (; _cursor < _line.length; _cursor++) { final char c0 = _line[_cursor]; if (c0 == '\\') { /* escaped value, hm, we do not unescape? */ _cursor++; /* skip next char */ continue; } if (c0 == '"') return _cursor; } return -1; /* did not find closing quote */ } /** * This skips whitespaces and searches for a leading hash ('#'). * * @param _line - the line to be checked * @return true if the line is a comment, false otherwise */ public int findFirstNonWhitespace(final String _line) { if (_line == null) return -1; final int len = _line.length(); if (len == 0) return -1; for (int i = 0; i < len; i++) { final char c0 = _line.charAt(i); if (!Character.isWhitespace(c0)) return i; } return -1; // no non-WS line was found } /* lines */ public String readUnfoldedLine() { StringBuilder sb = null; String line; this.lastException = null; try { while ((line = this.reader.readLine()) != null) { /** * Note: you may not use a comment in a folded line! Eg: * MyDirective \ * # my comment \ * MyArgument * This does not work. */ final boolean isFolded = line.endsWith("\\"); if (isFolded) /* cut off fold marker */ line = line.substring(0, line.length()); if (sb != null) { /* folding has happened on a previous line */ sb.append(line); } else if (isFolded) { /* first folded line, setup buffer */ sb = new StringBuilder(2048); sb.append(line); } if (!isFolded) break; /* we are done with unfolding */ } } catch (IOException e) { this.lastException = e; return null; } return sb != null ? sb.toString() : line; } }