/* 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 Collections * of Strings 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 Strings * 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(); } }