import java.net.*;
import java.io.*;
-// FIXME: this is important: folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace)
+// FEATURE: construct hash lazily?
public class Headers extends JS.Immutable implements Fountain {
- private final Hash head = new Hash();
- private final Hash headModified = new Hash();
- public int lines;
- public final boolean mime;
+
+ /**
+ * constructs a new set of Headers based on a preexisting set --
+ * keyval's even-numbered elements are keys, odd elements are
+ * values -- a null value deletes, non-null value replaces
+ */
+ public Headers(Headers old, String[] keyval) { this(old.updateHeaders(keyval), false); }
+ public Headers(String[] keyval) { this(new Headers(), keyval); }
+
+ public Headers() { this(new String[0]); }
+ public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
+ public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
+
+
+ // public //////////////////////////////////////////////////////////////////////////////
- private String raw;
- private StringFountain fountain;
+ public String[] getHeaderNames() { return (String[])head.dumpkeys(new String[head.size()]); }
+ public String get(String s) { return (String)head.get(s.toLowerCase()); }
+ public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
- public String get(String s) {
- String ret = (String)headModified.get(s.toLowerCase());
- if (ret==null) ret = (String)head.get(s.toLowerCase());
- return ret;
+ public Stream getStream() { return fountain().getStream(); }
+ public long getLength() { return fountain().getLength(); }
+ public int getNumLines() { return fountain().getNumLines(); }
+
+
+ // private //////////////////////////////////////////////////////////////////////////////
+
+ private final Hash head = new Hash();
+ private final boolean mime;
+ private final Entry[] entries;
+ private Fountain fountain = null;
+
+ private synchronized Fountain fountain() { // lazily constructed
+ if (fountain == null) {
+ StringBuffer sb = new StringBuffer();
+ for(Entry e : entries)
+ sb.append(e.toString());
+ this.fountain = Fountain.Util.create(sb.toString());
+ }
+ return fountain;
}
- public void remove(String k) { put(k, null); }
- public void put(String k, String v) {
- Stream stream = getStream();
- StringBuffer all = new StringBuffer();
- int lines = 0;
- String key = null;
- for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
- if (!Character.isSpace(s.charAt(0))) {
- if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
- key = s.substring(0, s.indexOf(':')).toLowerCase();
- }
- if (key.toLowerCase().equals(k.toLowerCase())) {
- if (v != null) { all.append(k + ": " + v + "\r\n"); lines++; v = null; }
- continue;
- }
- all.append(s);
- all.append("\r\n");
- lines++;
+
+ private static class Entry {
+ public final String key;
+ public final String val;
+ public String toString() { return key+":"+val; }
+ public Entry(String key, String val) throws Malformed {
+ this.key = key;
+ this.val = val;
+ for(int i=0; i<key.length(); i++)
+ if (key.charAt(i) < 33 || key.charAt(i) > 126)
+ throw new Malformed("Header key \""+key+"\" contains invalid character \"" +
+ key.charAt(i) + "\" (0x"+Integer.toString(key.charAt(i), 16) +")");
}
- if (v != null) {
- lines++;
- all.append(k + ": " + v + "\r\n");
+ }
+
+ private Headers(Entry[] entries, boolean assumeMime) {
+ this.entries = entries;
+ this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
+ for(Entry e : entries) {
+ String val = (String)head.get(e.key.toLowerCase());
+ val = val==null ? e.val.trim() : val+" "+e.val.trim(); // introduce folding whitespace =(
+ // FEATURE
+ //if (mime) k = Encode.RFC2047.decode(k);
+ //if (mime) v = Encode.RFC2047.decode(v);
+ head.put(e.key.toLowerCase(), val);
}
- all.append("\r\n");
- this.raw = all.toString();
- this.lines = lines;
- this.fountain = new Fountain.StringFountain(this.raw);
}
- public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
-
- public Stream getStream() { return fountain.getStream(); }
- public int getLength() { return fountain.getLength(); }
- public int getNumLines() { return fountain.getNumLines(); }
- public Stream getStreamWithCRLF() { return new Stream(raw+"\r\n"); }
-
- // FIXME
- public String getString() { return raw; }
-
- public Headers(Stream stream) throws Malformed { this(stream, false); }
- public Headers(Stream stream, boolean assumeMime) throws Malformed {
- StringBuffer all = new StringBuffer();
+
+ private static Entry[] extractEntries(Fountain fountain) throws Malformed {
String key = null;
- int lines = 0;
+ Stream stream = fountain.getStream();
+ ArrayList<Entry> entries = new ArrayList<Entry>();
for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
- all.append(s);
- all.append("\r\n");
- lines++;
+ s += "\r\n"; // this is the only place where we introduce manglage -- we normalize EOLs
if (Character.isSpace(s.charAt(0))) {
if (key == null) throw new Malformed("Message began with a blank line; no headers");
- head.put(key, head.get(key) + " " + s.trim());
+ Entry e = entries.remove(entries.size()-1);
+ entries.add(new Entry(e.key, e.val+s));
continue;
}
if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
- key = s.substring(0, s.indexOf(':')).toLowerCase();
- for(int i=0; i<key.length(); i++)
- if (key.charAt(i) < 33 || key.charAt(i) > 126)
- throw new Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\"");
- String val = s.substring(s.indexOf(':') + 1).trim();
- if (get(key) != null) val = get(key) + " " + val; // just append it to the previous one;
- head.put(key, val);
+ key = s.substring(0, s.indexOf(':'));
+ entries.add(new Entry(key, s.substring(s.indexOf(':') + 1)));
}
- this.raw = all.toString();
- this.fountain = new Fountain.StringFountain(this.raw);
- this.lines = lines;
- this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
- /*
- java.util.Enumeration e = head.keys();
- while(e.hasNext()) {
- String k = (String)e.next();
- String v = (String)head.get(k);
- if (mime) k = Encode.RFC2047.decode(k);
- v = uncomment(v);
- if (mime) v = Encode.RFC2047.decode(v);
- head.put(k, v);
- }
- */
+ return (Entry[])entries.toArray(new Entry[entries.size()]);
+ }
+
+ private Entry[] updateHeaders(String[] keyval) {
+ ArrayList<Entry> entries = new ArrayList<Entry>();
+ for(int i=0; i<this.entries.length; i++)
+ entries.add(this.entries[i]);
+ OUTER: for(int i=0; i<keyval.length; i+=2) {
+ for(int j=0; j<entries.size(); j++) {
+ Entry e = entries.get(j);
+ if (!e.key.toLowerCase().equals(keyval[i].toLowerCase())) continue;
+ if (keyval[i+1]==null)
+ entries.remove(j);
+ else
+ entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
+ continue OUTER;
+ }
+ if (keyval[i+1]!=null)
+ entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
+ }
+ return (Entry[])entries.toArray(new Entry[entries.size()]);
}
// Helpers //////////////////////////////////////////////////////////////////////////////
public static Stream skip(Stream stream) {
- for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
+ for(String s = stream.readln(); s!=null && s.trim().length() > 0;)
+ s = stream.readln();
return stream;
}
- public static String uncomment(String val) {
+ // designed to remove CFWS, but doesn't work right
+ public static String removeCFWS(String val) {
boolean inquotes = false;
for(int i=0; i<val.length(); i++) {
if (val.charAt(i) == '\"') inquotes = !inquotes;
+ // FIXME: nested comments
if (val.charAt(i) == '(' && !inquotes)
val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
}
return val;
}
+
}