/* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package com.apigee.sdk.apm.http.impl.client.cache; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.http.Header; import org.apache.http.impl.cookie.DateParseException; import org.apache.http.impl.cookie.DateUtils; /** * This class provides for parsing and understanding Warning headers. As the * Warning header can be multi-valued, but the values can contain separators * like commas inside quoted strings, we cannot use the regular * {@link Header#getElements()} call to access the values. */ class WarningValue { private int offs; private int init_offs; private String src; private int warnCode; private String warnAgent; private String warnText; private Date warnDate; WarningValue(String s) { this(s, 0); } WarningValue(String s, int offs) { this.offs = this.init_offs = offs; this.src = s; consumeWarnValue(); } /** * Returns an array of the parseable warning values contained in the given * header value, which is assumed to be a Warning header. Improperly * formatted warning values will be skipped, in keeping with the philosophy * of "ignore what you cannot understand." * * @param h * Warning {@link Header} to parse * @return array of <code>WarnValue</code> objects */ public static WarningValue[] getWarningValues(Header h) { List<WarningValue> out = new ArrayList<WarningValue>(); String src = h.getValue(); int offs = 0; while (offs < src.length()) { try { WarningValue wv = new WarningValue(src, offs); out.add(wv); offs = wv.offs; } catch (IllegalArgumentException e) { final int nextComma = src.indexOf(',', offs); if (nextComma == -1) break; offs = nextComma + 1; } } WarningValue[] wvs = {}; return out.toArray(wvs); } /* * LWS = [CRLF] 1*( SP | HT ) CRLF = CR LF */ protected void consumeLinearWhitespace() { while (offs < src.length()) { switch (src.charAt(offs)) { case '\r': if (offs + 2 >= src.length() || src.charAt(offs + 1) != '\n' || (src.charAt(offs + 2) != ' ' && src.charAt(offs + 2) != '\t')) { return; } offs += 2; break; case ' ': case '\t': break; default: return; } offs++; } } /* * CHAR = <any US-ASCII character (octets 0 - 127)> */ private boolean isChar(char c) { int i = (int) c; return (i >= 0 && i <= 127); } /* * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)> */ private boolean isControl(char c) { int i = (int) c; return (i == 127 || (i >= 0 && i <= 31)); } /* * separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> | * "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT */ private boolean isSeparator(char c) { return (c == '(' || c == ')' || c == '<' || c == '>' || c == '@' || c == ',' || c == ';' || c == ':' || c == '\\' || c == '\"' || c == '/' || c == '[' || c == ']' || c == '?' || c == '=' || c == '{' || c == '}' || c == ' ' || c == '\t'); } /* * token = 1*<any CHAR except CTLs or separators> */ protected void consumeToken() { if (!isTokenChar(src.charAt(offs))) parseError(); while (offs < src.length()) { if (!isTokenChar(src.charAt(offs))) break; offs++; } } private boolean isTokenChar(char c) { return (isChar(c) && !isControl(c) && !isSeparator(c)); } private static final String TOPLABEL = "\\p{Alpha}([\\p{Alnum}-]*\\p{Alnum})?"; private static final String DOMAINLABEL = "\\p{Alnum}([\\p{Alnum}-]*\\p{Alnum})?"; private static final String HOSTNAME = "(" + DOMAINLABEL + "\\.)*" + TOPLABEL + "\\.?"; private static final String IPV4ADDRESS = "\\d+\\.\\d+\\.\\d+\\.\\d+"; private static final String HOST = "(" + HOSTNAME + ")|(" + IPV4ADDRESS + ")"; private static final String PORT = "\\d*"; private static final String HOSTPORT = "(" + HOST + ")(\\:" + PORT + ")?"; private static final Pattern HOSTPORT_PATTERN = Pattern.compile(HOSTPORT); protected void consumeHostPort() { Matcher m = HOSTPORT_PATTERN.matcher(src.substring(offs)); if (!m.find()) parseError(); if (m.start() != 0) parseError(); offs += m.end(); } /* * warn-agent = ( host [ ":" port ] ) | pseudonym pseudonym = token */ protected void consumeWarnAgent() { int curr_offs = offs; try { consumeHostPort(); warnAgent = src.substring(curr_offs, offs); consumeCharacter(' '); return; } catch (IllegalArgumentException e) { offs = curr_offs; } consumeToken(); warnAgent = src.substring(curr_offs, offs); consumeCharacter(' '); } /* * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) qdtext = <any TEXT * except <">> */ protected void consumeQuotedString() { if (src.charAt(offs) != '\"') parseError(); offs++; boolean foundEnd = false; while (offs < src.length() && !foundEnd) { char c = src.charAt(offs); if (offs + 1 < src.length() && c == '\\' && isChar(src.charAt(offs + 1))) { offs += 2; // consume quoted-pair } else if (c == '\"') { foundEnd = true; offs++; } else if (c != '\"' && !isControl(c)) { offs++; } else { parseError(); } } if (!foundEnd) parseError(); } /* * warn-text = quoted-string */ protected void consumeWarnText() { int curr = offs; consumeQuotedString(); warnText = src.substring(curr, offs); } private static final String MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec"; private static final String WEEKDAY = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday"; private static final String WKDAY = "Mon|Tue|Wed|Thu|Fri|Sat|Sun"; private static final String TIME = "\\d{2}:\\d{2}:\\d{2}"; private static final String DATE3 = "(" + MONTH + ") ( |\\d)\\d"; private static final String DATE2 = "\\d{2}-(" + MONTH + ")-\\d{2}"; private static final String DATE1 = "\\d{2} (" + MONTH + ") \\d{4}"; private static final String ASCTIME_DATE = "(" + WKDAY + ") (" + DATE3 + ") (" + TIME + ") \\d{4}"; private static final String RFC850_DATE = "(" + WEEKDAY + "), (" + DATE2 + ") (" + TIME + ") GMT"; private static final String RFC1123_DATE = "(" + WKDAY + "), (" + DATE1 + ") (" + TIME + ") GMT"; private static final String HTTP_DATE = "(" + RFC1123_DATE + ")|(" + RFC850_DATE + ")|(" + ASCTIME_DATE + ")"; private static final String WARN_DATE = "\"(" + HTTP_DATE + ")\""; private static final Pattern WARN_DATE_PATTERN = Pattern.compile(WARN_DATE); /* * warn-date = <"> HTTP-date <"> */ protected void consumeWarnDate() { int curr = offs; Matcher m = WARN_DATE_PATTERN.matcher(src.substring(offs)); if (!m.lookingAt()) parseError(); offs += m.end(); try { warnDate = DateUtils.parseDate(src.substring(curr + 1, offs - 1)); } catch (DateParseException e) { throw new IllegalStateException("couldn't parse a parseable date"); } } /* * warning-value = warn-code SP warn-agent SP warn-text [SP warn-date] */ protected void consumeWarnValue() { consumeLinearWhitespace(); consumeWarnCode(); consumeWarnAgent(); consumeWarnText(); if (offs + 1 < src.length() && src.charAt(offs) == ' ' && src.charAt(offs + 1) == '\"') { consumeCharacter(' '); consumeWarnDate(); } consumeLinearWhitespace(); if (offs != src.length()) { consumeCharacter(','); } } protected void consumeCharacter(char c) { if (offs + 1 > src.length() || c != src.charAt(offs)) { parseError(); } offs++; } /* * warn-code = 3DIGIT */ protected void consumeWarnCode() { if (offs + 4 > src.length() || !Character.isDigit(src.charAt(offs)) || !Character.isDigit(src.charAt(offs + 1)) || !Character.isDigit(src.charAt(offs + 2)) || src.charAt(offs + 3) != ' ') { parseError(); } warnCode = Integer.parseInt(src.substring(offs, offs + 3)); offs += 4; } private void parseError() { String s = src.substring(init_offs); throw new IllegalArgumentException("Bad warn code \"" + s + "\""); } /** * Returns the 3-digit code associated with this warning. * * @return <code>int</code> */ public int getWarnCode() { return warnCode; } /** * Returns the "warn-agent" string associated with this warning, which is * either the name or pseudonym of the server that added this particular * Warning header. * * @return {@link String} */ public String getWarnAgent() { return warnAgent; } /** * Returns the human-readable warning text for this warning. Note that the * original quoted-string is returned here, including escaping for any * contained characters. In other words, if the header was: * * <pre> * Warning: 110 fred "Response is stale" * </pre> * * then this method will return <code>"\"Response is stale\""</code> * (surrounding quotes included). * * @return {@link String} */ public String getWarnText() { return warnText; } /** * Returns the date and time when this warning was added, or * <code>null</code> if a warning date was not supplied in the header. * * @return {@link Date} */ public Date getWarnDate() { return warnDate; } /** * Formats a <code>WarningValue</code> as a {@link String} suitable for * including in a header. For example, you can: * * <pre> * WarningValue wv = ...; * HttpResponse resp = ...; * resp.addHeader("Warning", wv.toString()); * </pre> * * @return {@link String} */ public String toString() { if (warnDate != null) { return String.format("%d %s %s \"%s\"", warnCode, warnAgent, warnText, DateUtils.formatDate(warnDate)); } else { return String.format("%d %s %s", warnCode, warnAgent, warnText); } } }