File: WebdavIfHeader.java

package info (click to toggle)
tomcat11 11.0.11-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 47,028 kB
  • sloc: java: 366,244; xml: 55,681; jsp: 4,783; sh: 1,304; perl: 324; makefile: 25; ansic: 14
file content (888 lines) | stat: -rw-r--r-- 33,637 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
/*
 * 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.Serial;
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 = "&lt;" absoluteURI "&gt;"
 * </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 = { "&lt;" Word "&gt;" Untagged } .
 *    Untagged = { "(" IfList ")" } .
 *    IfList = { [ "Not" ] ( ("&lt;" Word "&gt;" ) | ( "[" 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>&lt; &gt;</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>(&lt;token&gt; [etag])</code> only matches if the concrete token has the value
 * <code>token</code> and the concrete etag has the value <code>etag</code>. On the other hand, the <em>ifList</em>
 * <code>(Not &lt;notoken&gt;)</code> matches any token which is not <code>notoken</code> (in this case the concrete
 * value of the etag is not taken into consideration).
 * <p>
 * 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 final List<String> resources = new ArrayList<>();

    /**
     * The list of all positive tokens present in the If header.
     */
    private final List<String> allTokens = new ArrayList<>();

    /**
     * The list of all NOT tokens present in the If header.
     */
    private final List<String> allNotTokens = new ArrayList<>();

    private String uriPrefix;

    /**
     * Create an 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));
        StringBuilder b = new StringBuilder();
        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.isEmpty()) {

            int firstChar = 0;
            try (StringReader 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;
                }
            }

        } 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 whitespace
            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 {
        StringBuilder buf = new StringBuilder();

        // 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>&lt; &gt;</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 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" ] ( ("&lt;" Word "&gt;" ) | ( "[" Word "]" ) ) } .
     * </pre>
     * <p>
     */
    private static class IfList extends ArrayList<IfListEntry> {

        @Serial
        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 &lt; 0 || index &gt; 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 {

        @Serial
        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 none 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 = { "&lt;" Word "&gt;" "(" IfList ")" } .
     * </pre>
     */
    private class IfHeaderMap extends HashMap<String,IfHeaderList> implements IfHeaderInterface {

        @Serial
        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);
            }
        }
    }
}