WebdavIfHeader.java
/*
* 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.
*/
package org.apache.tomcat.util.http;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.res.StringManager;
/**
* The <code>IfHeader</code> class represents the state lists defined
* through the HTTP <em>If</em> header, which is specified in RFC 2518 as
* follows :
* <pre>
* If = "If" ":" ( 1*No-tag-list | 1*Tagged-list)
* No-tag-list = List
* Tagged-list = Resource 1*List
* Resource = Coded-URL
* List = "(" 1*(["Not"](State-etag | "[" entity-tag "]")) ")"
* State-etag = Coded-URL
* Coded-URL = "<" absoluteURI ">"
* </pre>
* <p>
* Reformulating this specification into proper EBNF as specified by N. Wirth
* we get the following productions, which map to the parse METHODS of this
* class. Any whitespace is ignored except for white space surrounding and
* within words which is considered significant.
* <pre>
* If = "If:" ( Tagged | Untagged ).
* Tagged = { "<" Word ">" Untagged } .
* Untagged = { "(" IfList ")" } .
* IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
* Word = characters .
* </pre>
* <p>
* An <em>If</em> header either contains untagged <em>IfList</em> entries or
* tagged <em>IfList</em> entries but not a mixture of both. An <em>If</em>
* header containing tagged entries is said to be of <em>tagged</em> type while
* an <em>If</em> header containing untagged entries is said to be of
* <em>untagged</em> type.
* <p>
* An <em>IfList</em> is a list of tokens - words enclosed in <em>< ></em>
* - and etags - words enclosed in <em>[ ]</em>. An <em>IfList</em> matches a
* (token, etag) tuple if all entries in the list match. If an entry in the list
* is prefixed with the word <em>Not</em> (parsed case insensitively) the entry
* must not match the concrete token or etag.
* <p>
* Example: The <em>ifList</em> <code>(<token> [etag])</code> only matches
* if the concret token has the value <code>token</code> and the conrete etag
* has the value <code>etag</code>. On the other hand, the <em>ifList</em>
* <code>(Not <notoken>)</code> matches any token which is not
* <code>notoken</code> (in this case the concrete value of the etag is
* not taken into consideration).
*
* This class was contributed by Apache Jackrabbit
*
* @author Felix Meschberger
*/
public class WebdavIfHeader {
private static final Log log = LogFactory.getLog(WebdavIfHeader.class);
private static final StringManager sm =
StringManager.getManager(WebdavIfHeader.class.getPackage().getName());
/**
* The string representation of the header value
*/
private final String headerValue;
/**
* The list of untagged state entries
*/
private final IfHeaderInterface ifHeader;
/**
* The list of resources present in the If header.
*/
private List<String> resources = new ArrayList<>();
/**
* The list of all positive tokens present in the If header.
*/
private List<String> allTokens = new ArrayList<>();
/**
* The list of all NOT tokens present in the If header.
*/
private List<String> allNotTokens = new ArrayList<>();
private String uriPrefix;
/**
* Create a Untagged <code>IfHeader</code> if the given lock tokens.
*
* @param tokens the tokens
*
* @throws IOException If the parsing of the <code>IfHeader</code> fails
*/
public WebdavIfHeader(String[] tokens) throws IOException {
allTokens.addAll(Arrays.asList(tokens));
StringBuffer b = new StringBuffer();
for (String token : tokens) {
b.append("(").append("<");
b.append(token);
b.append(">").append(")");
}
headerValue = b.toString();
ifHeader = parse();
}
/**
* Parses the <em>If</em> header and creates and internal representation
* which is easy to query.
*
* @param uriPrefix The uri prefix to use for the absolute href
* @param ifHeaderValue the if header
*
* @throws IOException If the parsing of the <code>IfHeader</code> fails
*/
public WebdavIfHeader(String uriPrefix, String ifHeaderValue) throws IOException {
this.uriPrefix = uriPrefix;
headerValue = ifHeaderValue;
ifHeader = parse();
}
/**
* @return If String.
*/
public String getHeaderName() {
return "If";
}
/**
* Return the String representation of the If header present on
* the given request or <code>null</code>.
*
* @return If header value as String or <code>null</code>.
*/
public String getHeaderValue() {
return headerValue;
}
/**
* Returns true if an If header was present in the given request. False otherwise.
*
* @return true if an If header was present.
*/
public boolean hasValue() {
return ifHeader != null;
}
/**
* Tries to match the contents of the <em>If</em> header with the given
* token and etag values with the restriction to only check for the tag.
* <p>
* If the <em>If</em> header is of untagged type, the untagged <em>IfList</em>
* is matched against the token and etag given: A match of the token and
* etag is found if at least one of the <em>IfList</em> entries match the
* token and etag tuple.
*
* @param tag The tag to identify the <em>IfList</em> to match the token
* and etag against.
* @param tokens The tokens to compare.
* @param etag The ETag value to compare.
*
* @return If the <em>If</em> header is of untagged type the result is
* <code>true</code> if any of the <em>IfList</em> entries matches
* the token and etag values. For tagged type <em>If</em> header the
* result is <code>true</code> if either no entry for the given tag
* exists in the <em>If</em> header or if the <em>IfList</em> for the
* given tag matches the token and etag given.
*/
public boolean matches(String tag, List<String> tokens, String etag) {
if (ifHeader == null) {
if (log.isTraceEnabled()) {
log.trace("matches: No If header, assume match");
}
return true;
} else {
return ifHeader.matches(tag, tokens, etag);
}
}
/**
* @return an iterator over all resources present in the if header.
*/
public Iterator<String> getResources() {
return resources.iterator();
}
/**
* @return an iterator over all tokens present in the if header, that were
* not denied by a leading NOT statement.
*/
public Iterator<String> getAllTokens() {
return allTokens.iterator();
}
/**
* @return an iterator over all NOT tokens present in the if header, that
* were explicitly denied.
*/
public Iterator<String> getAllNotTokens() {
return allNotTokens.iterator();
}
/**
* Parse the original header value and build the internal IfHeaderInterface
* object that is easy to query.
*/
private IfHeaderInterface parse()
throws IOException {
IfHeaderInterface ifHeader;
if (headerValue != null && headerValue.length() > 0) {
StringReader reader = null;
int firstChar = 0;
try {
reader = new StringReader(headerValue);
// get the first character to decide - expect '(' or '<'
try {
reader.mark(1);
firstChar = readWhiteSpace(reader);
reader.reset();
} catch (IOException ignore) {
// may be thrown according to API but is only thrown by the
// StringReader class if the reader is already closed.
}
if (firstChar == '(') {
ifHeader = parseUntagged(reader);
} else if (firstChar == '<') {
ifHeader = parseTagged(reader);
} else {
logIllegalState("If", firstChar, "(<", null);
ifHeader = null;
}
} finally {
if (reader != null) {
reader.close();
}
}
} else {
if (log.isTraceEnabled()) {
log.trace("IfHeader: No If header in request");
}
ifHeader = null;
}
return ifHeader;
}
//---------- internal IF header parser -------------------------------------
/**
* Parses a tagged type <em>If</em> header. This method implements the
* <em>Tagged</em> production given in the class comment :
* <pre>
* Tagged = { "<" Word ">" Untagged } .
* </pre>
*
* @param reader the reader
* @return the parsed map
*/
private IfHeaderMap parseTagged(StringReader reader)
throws IOException {
IfHeaderMap map = new IfHeaderMap();
while (true) {
// read next non-white space
int c = readWhiteSpace(reader);
if (c < 0) {
// end of input, no more entries
break;
} else if (c == '<') {
// start a tag with an IfList
String resource = readWord(reader, '>');
if (resource != null) {
// go to untagged after reading the resource
map.put(resource, parseUntagged(reader));
resources.add(resource);
} else {
break;
}
} else {
// unexpected character
// catchup to end of input or start of a tag
logIllegalState("Tagged", c, "<", reader);
}
}
return map;
}
/**
* Parses an untagged type <em>If</em> header. This method implements the
* <em>Untagged</em> production given in the class comment :
* <pre>
* Untagged = { "(" IfList ")" } .
* </pre>
*
* @param reader The <code>StringReader</code> to read from for parsing
*
* @return An <code>ArrayList</code> of {@link IfList} entries.
*/
private IfHeaderList parseUntagged(StringReader reader)
throws IOException {
IfHeaderList list = new IfHeaderList();
while (true) {
// read next non white space
reader.mark(1);
int c = readWhiteSpace(reader);
if (c < 0) {
// end of input, no more IfLists
break;
} else if (c == '(') {
// start of an IfList, parse
list.add(parseIfList(reader));
} else if (c == '<') {
// start of a tag, return current list
reader.reset();
break;
} else {
// unexpected character
// catchup to end of input or start of an IfList
logIllegalState("Untagged", c, "(", reader);
}
}
return list;
}
/**
* Parses an <em>IfList</em> in the <em>If</em> header. This method
* implements the <em>Tagged</em> production given in the class comment :
* <pre>
* IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
* </pre>
*
* @param reader The <code>StringReader</code> to read from for parsing
*
* @return The {@link IfList} for the input <em>IfList</em>.
*
* @throws IOException if a problem occurs during reading.
*/
private IfList parseIfList(StringReader reader) throws IOException {
IfList res = new IfList();
boolean positive = true;
String word;
ReadLoop:
while (true) {
int nextChar = readWhiteSpace(reader);
switch (nextChar) {
case 'N':
case 'n':
// read not
// check whether o or O
int not = reader.read();
if (not != 'o' && not != 'O') {
logIllegalState("IfList-Not", not, "o", null);
break;
}
// check whether t or T
not = reader.read();
if (not !='t' && not != 'T') {
logIllegalState("IfList-Not", not, "t", null);
break;
}
// read Not ok
positive = false;
break;
case '<':
// state token
word = readWord(reader, '>');
if (word != null) {
res.add(new IfListEntryToken(word, positive));
// also add the token to the list of all tokens
if (positive) {
allTokens.add(word);
} else {
allNotTokens.add(word);
}
positive = true;
}
break;
case '[':
// etag
word = readWord(reader, ']');
if (word != null) {
res.add(new IfListEntryEtag(word, positive));
positive = true;
}
break;
case ')':
// correct end of list, end the loop
if (log.isTraceEnabled()) {
log.trace("parseIfList: End of If list, terminating loop");
}
break ReadLoop;
default:
logIllegalState("IfList", nextChar, "nN<[)", reader);
// abort loop if EOF
if (nextChar < 0) {
break ReadLoop;
}
break;
}
}
// return the current list anyway
return res;
}
/**
* Returns the first non-whitespace character from the reader or -1 if
* the end of the reader is encountered.
*
* @param reader The <code>Reader</code> to read from
*
* @return The first non-whitespace character or -1 in case of EOF.
*
* @throws IOException if a problem occurs during reading.
*/
private int readWhiteSpace(Reader reader) throws IOException {
int c = reader.read();
while (c >= 0 && Character.isWhitespace((char) c)) {
c = reader.read();
}
return c;
}
/**
* Reads from the input until the end character is encountered and returns
* the string up to but not including this end character. If the end of input
* is reached before reading the end character <code>null</code> is
* returned.
* <p>
* Note that this method does not support any escaping.
*
* @param reader The <code>Reader</code> to read from
* @param end The ending character limiting the word.
*
* @return The string read up to but not including the ending character or
* <code>null</code> if the end of input is reached before the ending
* character has been read.
*
* @throws IOException if a problem occurs during reading.
*/
private String readWord(Reader reader, char end) throws IOException {
StringBuffer buf = new StringBuffer();
// read the word value
int c = reader.read();
for (; c >= 0 && c != end; c=reader.read()) {
buf.append((char) c);
}
// check whether we succeeded
if (c < 0) {
log.error("readWord: Unexpected end of input reading word");
return null;
}
// build the string and return it
return buf.toString();
}
/**
* Logs an unexpected character with the corresponding state and list of
* expected characters. If the reader parameter is not null, characters
* are read until either the end of the input is reached or any of the
* characters in the expChar string is read.
*
* @param state The name of the current parse state. This method logs this
* name in the message. The intended value would probably be the
* name of the EBNF production during which the error occurs.
* @param effChar The effective character read.
* @param expChar The list of characters acceptable in the current state.
* @param reader The reader to be caught up to any of the expected
* characters. If <code>null</code> the input is not caught up to
* any of the expected characters (of course ;-).
*/
private void logIllegalState(String state, int effChar, String expChar,
StringReader reader) {
// format the effective character to be logged
String effString = (effChar < 0) ? "<EOF>" : String.valueOf((char) effChar);
// log the error
log.error(sm.getString("webdavifheader.unexpectedCharacter", effString, state, expChar));
// catch up if a reader is given
if (reader != null && effChar >= 0) {
try {
if (log.isTraceEnabled()) {
log.trace("logIllegalState: Catch up to any of "+expChar);
}
do {
reader.mark(1);
effChar = reader.read();
} while (effChar >= 0 && expChar.indexOf(effChar) < 0);
if (effChar >= 0) {
reader.reset();
}
} catch (IOException ioe) {
log.error(sm.getString("webdavifheader.ioError", expChar));
}
}
}
//---------- internal If header structure ----------------------------------
/**
* The <code>IfListEntry</code> abstract class is the base class for
* entries in an <em>IfList</em> production. This abstract base class
* provides common functionality to both types of entries, namely tokens
* enclosed in angle brackets (<code>< ></code>) and etags enclosed
* in square brackets (<code>[ ]</code>).
*/
private abstract static class IfListEntry {
/**
* The entry string value - the semantics of this value depends on the
* implementing class.
*/
protected final String value;
/** Flag to indicate, whether this is a positive match or not */
protected final boolean positive;
/** The cached result of the {@link #toString} method. */
protected String stringValue;
/**
* Sets up the final fields of this abstract class. The meaning of
* value parameter depends solely on the implementing class. From the
* point of view of this abstract class, it is simply a string value.
*
* @param value The string value of this instance
* @param positive <code>true</code> if matches are positive
*/
protected IfListEntry(String value, boolean positive) {
this.value = value;
this.positive = positive;
}
/**
* Matches the value from the parameter to the internal string value.
* If the parameter and the {@link #value} field match, the method
* returns <code>true</code> for positive matches and <code>false</code>
* for negative matches.
* <p>
* This helper method can be called by implementations to evaluate the
* concrete match on the correct value parameter. See
* {@link #match(String, String)} for the external API method.
*
* @param value The string value to compare to the {@link #value}
* field.
*
* @return <code>true</code> if the value parameter and the
* {@link #value} field match and the {@link #positive} field is
* <code>true</code> or if the values do not match and the
* {@link #positive} field is <code>false</code>.
*/
protected boolean match(String value) {
return positive == this.value.equals(value);
}
/**
* Matches the entry's value to the the token or etag. Depending on the
* concrete implementation, only one of the parameters may be evaluated
* while the other may be ignored.
* <p>
* Implementing METHODS may call the helper method {@link #match(String)}
* for the actual matching.
*
* @param token The token value to compare
* @param etag The etag value to compare
*
* @return <code>true</code> if the token/etag matches the <em>IfList</em>
* entry.
*/
public abstract boolean match(String token, String etag);
/**
* Returns a short type name for the implementation. This method is
* used by the {@link #toString} method to build the string representation
* if the instance.
*
* @return The type name of the implementation.
*/
protected abstract String getType();
/**
* @return the value of this entry
*/
protected String getValue() {
return value;
}
/**
* Returns the String representation of this entry. This method uses the
* {@link #getType} to build the string representation.
*
* @return the String representation of this entry.
*/
@Override
public String toString() {
if (stringValue == null) {
stringValue = getType() + ": " + (positive ? "" : "!") + getValue();
}
return stringValue;
}
}
/**
* The <code>IfListEntryToken</code> extends the {@link IfListEntry}
* abstract class to represent an entry for token matching.
*/
private static class IfListEntryToken extends IfListEntry {
/**
* Creates a token matching entry.
*
* @param token The token value pertinent to this instance.
* @param positive <code>true</code> if this is a positive match entry.
*/
IfListEntryToken(String token, boolean positive) {
super(token, positive);
}
/**
* Matches the token parameter to the stored token value and returns
* <code>true</code> if the values match and if the match is positive.
* <code>true</code> is also returned for negative matches if the values
* do not match.
*
* @param token The token value to compare
* @param etag The etag value to compare, which is ignored in this
* implementation.
*
* @return <code>true</code> if the token matches the <em>IfList</em>
* entry's token value.
*/
@Override
public boolean match(String token, String etag) {
return token == null || super.match(token);
}
/**
* Returns the type name of this implementation, which is fixed to
* be <em>Token</em>.
*
* @return The fixed string <em>Token</em> as the type name.
*/
@Override
protected String getType() {
return "Token";
}
}
/**
* The <code>IfListEntryToken</code> extends the {@link IfListEntry}
* abstract class to represent an entry for etag matching.
*/
private static class IfListEntryEtag extends IfListEntry {
/**
* Creates an etag matching entry.
*
* @param etag The etag value pertinent to this instance.
* @param positive <code>true</code> if this is a positive match entry.
*/
IfListEntryEtag(String etag, boolean positive) {
super(etag, positive);
}
/**
* Matches the etag parameter to the stored etag value and returns
* <code>true</code> if the values match and if the match is positive.
* <code>true</code> is also returned for negative matches if the values
* do not match.
*
* @param token The token value to compare, which is ignored in this
* implementation.
* @param etag The etag value to compare
*
* @return <code>true</code> if the etag matches the <em>IfList</em>
* entry's etag value.
*/
@Override
public boolean match(String token, String etag) {
return super.match(etag);
}
/**
* Returns the type name of this implementation, which is fixed to
* be <em>ETag</em>.
*
* @return The fixed string <em>ETag</em> as the type name.
*/
@Override
protected String getType() {
return "ETag";
}
}
/**
* The <code>IfList</code> class extends the <code>ArrayList</code> class
* with the limitation to only support adding {@link IfListEntry} objects
* and adding a {@link #match} method.
* <p>
* This class is a container for data contained in the <em>If</em>
* production <em>IfList</em>
* <pre>
* IfList = { [ "Not" ] ( ("<" Word ">" ) | ( "[" Word "]" ) ) } .
* </pre>
* <p>
*/
private static class IfList extends ArrayList<IfListEntry> {
private static final long serialVersionUID = 1L;
/**
* Adds the {@link IfListEntry} at the end of the list.
*
* @param entry The {@link IfListEntry} to add to the list
*
* @return <code>true</code> (as per the general contract of Collection.add).
*/
@Override
public boolean add(IfListEntry entry) {
return super.add(entry);
}
/**
* Adds the {@link IfListEntry} at the indicated position of the list.
*
* @param index the index
* @param entry the entry
*
* @throws IndexOutOfBoundsException if index is out of range
* <code>(index < 0 || index > size())</code>.
*/
@Override
public void add(int index, IfListEntry entry) {
super.add(index, entry);
}
/**
* Returns <code>true</code> if all {@link IfListEntry} objects in the
* list match the given token and etag. If the list is entry, it is
* considered to match the token and etag.
*
* @param tokens The token to compare.
* @param etag The etag to compare.
*
* @return <code>true</code> if all entries in the list match the
* given tag and token.
*/
public boolean match(List<String> tokens, String etag) {
if (log.isTraceEnabled()) {
log.trace("match: Trying to match token=" + tokens + ", etag=" + etag);
}
for (int i=0; i < size(); i++) {
IfListEntry ile = get(i);
boolean match = false;
for (String token : tokens) {
if (ile.match(token, etag)) {
match = true;
}
}
if (!match) {
if (log.isTraceEnabled()) {
log.trace("match: Entry " + i + "-" + ile + " does not match");
}
return false;
}
}
// invariant: all entries matched
return true;
}
}
/**
* The <code>IfHeaderInterface</code> interface abstracts away the difference of
* tagged and untagged <em>If</em> header lists. The single method provided
* by this interface is to check whether a request may be applied to a
* resource with given token and etag.
*/
private interface IfHeaderInterface {
/**
* Matches the resource, token, and etag against this
* <code>IfHeaderInterface</code> instance.
*
* @param resource The resource to match this instance against. This
* must be absolute URI of the resource as defined in Section 3
* (URI Syntactic Components) of RFC 2396 Uniform Resource
* Identifiers (URI): Generic Syntax.
* @param tokens The resource's lock token to match
* @param etag The resource's etag to match
*
* @return <code>true</code> if the header matches the resource with
* token and etag, which means that the request is applicable
* to the resource according to the <em>If</em> header.
*/
boolean matches(String resource, List<String> tokens, String etag);
}
/**
* The <code>IfHeaderList</code> class implements the {@link IfHeaderInterface}
* interface to support untagged lists of {@link IfList}s. This class
* implements the data container for the production :
* <pre>
* Untagged = { "(" IfList ")" } .
* </pre>
*/
private static class IfHeaderList extends ArrayList<IfList> implements IfHeaderInterface {
private static final long serialVersionUID = 1L;
/**
* Matches a list of {@link IfList}s against the token and etag. If any of
* the {@link IfList}s matches, the method returns <code>true</code>.
* On the other hand <code>false</code> is only returned if non of the
* {@link IfList}s match.
*
* @param resource The resource to match, which is ignored by this
* implementation. A value of <code>null</code> is therefor
* acceptable.
* @param tokens The tokens to compare.
* @param etag The ETag value to compare.
*
* @return <code>True</code> if any of the {@link IfList}s matches the token
* and etag, else <code>false</code> is returned.
*/
@Override
public boolean matches(String resource, List<String> tokens, String etag) {
if (log.isTraceEnabled()) {
log.trace("matches: Trying to match token=" + tokens + ", etag=" + etag);
}
for (IfList il : this) {
if (il.match(tokens, etag)) {
if (log.isTraceEnabled()) {
log.trace("matches: Found match with " + il);
}
return true;
}
}
// invariant: no match found
return false;
}
}
/**
* The <code>IfHeaderMap</code> class implements the {@link IfHeaderInterface}
* interface to support tagged lists of {@link IfList}s. This class
* implements the data container for the production :
* <pre>
* Tagged = { "<" Word ">" "(" IfList ")" } .
* </pre>
*/
private class IfHeaderMap extends HashMap<String, IfHeaderList> implements IfHeaderInterface {
private static final long serialVersionUID = 1L;
/**
* Matches the token and etag for the given resource. If the resource is
* not mentioned in the header, a match is assumed and <code>true</code>
* is returned in this case.
*
* @param resource The absolute URI of the resource for which to find
* a match.
* @param tokens The tokens to compare.
* @param etag The etag to compare.
*
* @return <code>true</code> if either no entry exists for the resource
* or if the entry for the resource matches the token and etag.
*/
@Override
public boolean matches(String resource, List<String> tokens, String etag) {
if (log.isTraceEnabled()) {
log.trace("matches: Trying to match resource=" + resource + ", token=" + tokens + "," + etag);
}
String uri;
String path;
if (resource.startsWith("/")) {
path = resource;
uri = WebdavIfHeader.this.uriPrefix + resource;
} else {
path = resource.substring(WebdavIfHeader.this.uriPrefix.length());
uri = resource;
}
IfHeaderList list = get(path);
if (list == null) {
list = get(uri);
}
if (list == null) {
if (log.isTraceEnabled()) {
log.trace("matches: No entry for tag " + resource + ", assuming mismatch");
}
return false;
} else {
return list.matches(resource, tokens, etag);
}
}
}
}