/* HeaderUtil.java * * Copyright (c) 2002, Benja Fallenstein * * This file is part of Gzz. * * Gzz 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 of the License, or * (at your option) any later version. * * Gzz 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 Gzz; if not, write to the Free * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA * * */ /* * Written by Benja Fallenstein */ package org.nongnu.storm.util; import java.io.*; import java.util.*; /** Utility functions for reading and writing RFC 2822-style headers. * The parsing functions in this util are strict in that they don't * try to handle non-conformant headers (e.g., headers that use * only CR or LF instead of CRLF). They also aren't fully conformant * because they do not currently attempt to parse obsoleted syntax, * as required by RFC 2822; support for this may be added * in the future. *
* So far, there is no support at all for structured field values.
*/
public class HeaderUtil {
/** Read the unstructured values of all fields in a header.
* Returns a Map
from canonicalized
* String
field names to Collection
s
* of String
s representing unstructured field
* values (with folding removed). For each field name, there
* is one entry in the corresponding Collection
* for each time that field appears in the header (this is
* necessary for supporting e.g. the Received:
* header field of which more than one instance may appear
* in a single header). If a field does not appear in the
* header at all, there is no mapping for its field name
* in the returned Map
.
*
* After this method, in
is positioned
* at the beginning of the message body, after the empty line
* ending the header.
*
* For a convenient way of extracting the values of header fields
* which are supposed to occur exactly once, see
* the getOne()
method.
*/
public static Map readHeader(InputStream in) throws IOException {
Map fields = new HashMap();
String fieldName = null;
String fieldBody = null;
while(true) {
String line = readLine(in);
if(line.equals("")) return fields;
if(line.trim().equals(""))
throw new ParseException("Line containing only whitespace " +
"in headers");
if(!Character.isSpace(line.charAt(0))) {
int i = line.indexOf(':');
if(i <= 0)
throw new ParseException("No header name in line '" +
line + "'");
fieldName = line.substring(0, i).toLowerCase();
fieldBody = line.substring(i+1);
list(fields, fieldName).add(0, fieldBody); // add field
} else if(fieldName == null) {
throw new ParseException("First line in header is folded");
} else {
fieldBody += line;
list(fields, fieldName).set(0, fieldBody); // replace field
}
}
}
/** Read in the raw header into a String.
*/
public static String readRaw(InputStream in) throws IOException {
StringBuffer sb = new StringBuffer();
String line;
do {
line = readLine(in);
sb.append(line);
sb.append((char)CR); sb.append((char)LF);
} while(!line.equals(""));
return sb.toString();
}
/** Return the value of a single header field, trimmed.
* This takes a fields
Map
is returned by
* readHeader()
, and extracts the field value of a single
* header field. The caller assumes that the field appears in
* the header exactly once; if this is not true, a
* ParseException
is thrown.
*
* For convenience, the returned String
is
* trim()
med, i.e., whitespace at the beginning and end
* is stripped off. If this is not desired, the value has to be
* extracted from the Map
manually.
*
* The fieldName
is canonicalized before looking it up
* in the fields
Map
.
*/
public static String getOne(Map fields, String fieldName)
throws ParseException {
List l = (List)fields.get(fieldName.toLowerCase());
if(l == null || l.isEmpty())
throw new ParseException("No '"+fieldName+"' header field");
if(l.size() > 1)
throw new ParseException("Unexpectedly, there is more than one " +
"'"+fieldName+"' header field " +
"in the header.");
return ((String)l.get(0)).trim();
}
/** Write a header given a Collection
of header fields,
* inserting CRLF after each one.
* This is mostly a convenient way of joining together a collection
* of lines. Additionally, checks are run that the length of each line
* does not exceed 998 characters, and that no line contains
* a CR and/or LF character. Folding has to be done outside this
* method.
*
* The lines are written in the order they're returned by the
* lines
Collection
's iterator.
* lines
can simply be a List
, but note
* that when using a SortedSet
,
* the same set of header fields is always turned
* into the same header (same as in same sequence of bytes)--
* no matter in which order the header fields are added
* to the collection.
* @param lines A Collection
of String
s
* representing lines to be written into the header.
* The Strings may not contain line breaks.
*/
public static void writeHeader(OutputStream out, Collection lines)
throws IOException {
for(Iterator i=lines.iterator(); i.hasNext();) {
String s = (String)i.next();
if(s.length() > 998)
throw new ParseException("Header line contains more than " +
"998 characters");
if(s.indexOf(CR) > 0)
throw new ParseException("Header line contains CR");
if(s.indexOf(LF) > 0)
throw new ParseException("Header line contains LF");
out.write(s.getBytes("US-ASCII"));
out.write(CR); out.write(LF);
}
out.write(CR); out.write(LF);
}
// ParseException
public static class ParseException extends IOException {
public ParseException(String s) { super(s); }
}
// Private stuff
private static List list(Map map, Object key) {
List l = (List)map.get(key);
if(l == null) {
l = new ArrayList(1);
map.put(key, l);
}
return l;
}
private static int CR = 0xd, LF = 0xa;
private static String readLine(InputStream in) throws IOException {
StringBuffer s = new StringBuffer();
while(true) {
int c = in.read();
if(c < 0)
throw new ParseException("EOF while reading headers");
if(c == CR) break;
if(c == LF)
throw new ParseException("LF without preceding CR");
if(c < 1 || c > 127)
throw new ParseException("Character outside US-ASCII " +
"1-127: '"+c+"'");
s.append((char)c);
}
int i = in.read();
if(i < 0)
throw new ParseException("EOF while reading headers");
if(i != LF)
throw new ParseException("CR not followed by LF");
return s.toString();
}
}