001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    /*
031     * Created by IntelliJ IDEA.
032     * User: Administrator
033     * Date: 28-Dec-02
034     * Time: 22:24:51
035     */
036    package org.hd.d.pg2k.test.dev;
037    
038    import java.text.SimpleDateFormat;
039    import java.util.Arrays;
040    import java.util.Collections;
041    import java.util.Date;
042    import java.util.HashMap;
043    import java.util.List;
044    import java.util.Locale;
045    import java.util.Map;
046    import java.util.Set;
047    import java.util.Vector;
048    
049    import javax.servlet.http.HttpServletRequest;
050    import javax.servlet.http.HttpServletResponse;
051    
052    import junit.framework.TestCase;
053    
054    import org.apache.http.message.BasicHeader;
055    import org.hd.d.pg2k.svrCore.ExhibitAttrUtils;
056    import org.hd.d.pg2k.svrCore.ExhibitStaticAttr;
057    import org.hd.d.pg2k.svrCore.Name;
058    import org.hd.d.pg2k.svrCore.TextUtils;
059    import org.hd.d.pg2k.webSvr.exhibit.ServletUtils;
060    import org.hd.d.pg2k.webSvr.exhibit.SimpleSimilarExhibitFinder;
061    import org.hd.d.pg2k.webSvr.util.WebUtils;
062    
063    /**Tests of WAR utility routines, mainly from the WebUtils class. */
064    public final class WebUtilsTest extends TestCase
065        {
066        public WebUtilsTest(final String name)
067            {
068            super(name);
069            }
070    
071        /**Tests of SimpleSimilarExhibitFinder class.
072         * This class is meant as the fast way to find some similar exhibits
073         * when the system is too busy for a full search to be practical.
074         * <p>
075         * The results of this simple search must be sane, if not always wonderful.
076         */
077        public void testSimpleSimilarExhibitFinder()
078            {
079            final Name.ExhibitFull THIS_EXNAME = Name.ExhibitFull.create("m/m-MM.jpg");
080    
081            final ExhibitAttrUtils.ExhibitAttrWords attrWords = ExhibitAttrUtils.getAttrWords();
082    
083            // Test on an empty List; test that we get a benign empty return.
084            final List<Name.ExhibitFull> emptyExhibitsList = Collections.emptyList();
085            final Set<String> emptyAttrWords = Collections.emptySet();
086            final Name.ExhibitFull r1[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
087                    emptyExhibitsList, TextUtils.CASE_INSENSITIVE_ORDER, THIS_EXNAME, emptyAttrWords, Integer.MAX_VALUE);
088            assertTrue("result should not be null", r1 != null);
089            assertTrue("on empty input list result should be empty", r1.length == 0);
090    
091            // Test on an List with only given exhibit; test that we get an empty return.
092            final Name.ExhibitFull r2[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
093                Collections.singletonList(THIS_EXNAME), TextUtils.CASE_INSENSITIVE_ORDER, THIS_EXNAME, emptyAttrWords, Integer.MAX_VALUE);
094            assertTrue("result should not be null", r2 != null);
095            assertTrue("on singleton input list result should be empty", r2.length == 0);
096    
097            final long now = System.currentTimeMillis();
098            final Map<Name.ExhibitFull,ExhibitStaticAttr> exhibits1 = new HashMap<Name.ExhibitFull, ExhibitStaticAttr>();
099            exhibits1.put(THIS_EXNAME, new ExhibitStaticAttr(THIS_EXNAME, 100, now));
100    //        final AllExhibitProperties aep1 = new AllExhibitProperties(
101    //                                new ExhibitPropsGlobalImmutable(),
102    //                                new LocationMap(),
103    //                                new AllExhibitImmutableData(exhibits1, now),
104    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
105            final Name.ExhibitFull r3[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
106                Collections.singletonList(THIS_EXNAME), attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
107            assertTrue("result should not be null", r3 != null);
108            assertTrue("on singleton input list result should be empty", r3.length == 0);
109    
110            // Add some more more entries with different main words.
111            // So this should still give no answers...
112            exhibits1.put(Name.ExhibitFull.create("a/a-AA.jpg"), new ExhibitStaticAttr(Name.ExhibitFull.create("a/a-AA.jpg"), 100, now));
113            exhibits1.put(Name.ExhibitFull.create("z/z-ZZ.jpg"), new ExhibitStaticAttr(Name.ExhibitFull.create("z/z-ZZ.jpg"), 100, now));
114    //        final AllExhibitProperties aep4 = new AllExhibitProperties(
115    //                                new ExhibitPropsGlobalImmutable(),
116    //                                new LocationMap(),
117    //                                new AllExhibitImmutableData(exhibits1, now),
118    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
119            final List<Name.ExhibitFull> l4 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
120            Collections.sort(l4, attrWords.SMART_ORDER);
121            final Name.ExhibitFull r4[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
122                l4, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
123            assertTrue("result should not be null", r4 != null);
124            assertTrue("on input list with only different main words result should be empty", r4.length == 0);
125    
126            // Add a variant of the main exhibit name,
127            // which should not add any results...
128            final Name.ExhibitFull VARIANT1 = Name.ExhibitFull.create("m/m-bg-MM.jpg");
129            exhibits1.put(VARIANT1, new ExhibitStaticAttr(VARIANT1, 100, now));
130    //        final AllExhibitProperties aep5 = new AllExhibitProperties(
131    //                                new ExhibitPropsGlobalImmutable(),
132    //                                new LocationMap(),
133    //                                new AllExhibitImmutableData(exhibits1, now),
134    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
135            final List<Name.ExhibitFull> l5 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
136            Collections.sort(l5, attrWords.SMART_ORDER);
137            final Name.ExhibitFull r5[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
138                l5, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
139            assertTrue("result should not be null", r5 != null);
140            assertTrue("on input list with only variants and different main words result should be empty", r5.length == 0);
141    
142            // Add a different exhibit with the same main word,
143            // which should now appear in the results...
144            final Name.ExhibitFull EXHIBIT2 = Name.ExhibitFull.create("m/m-yyy-MM.jpg");
145            exhibits1.put(EXHIBIT2, new ExhibitStaticAttr(EXHIBIT2, 100, now));
146    //        final AllExhibitProperties aep6 = new AllExhibitProperties(
147    //                                new ExhibitPropsGlobalImmutable(),
148    //                                new LocationMap(),
149    //                                new AllExhibitImmutableData(exhibits1, now),
150    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
151            final List<Name.ExhibitFull> l6 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
152            Collections.sort(l6, attrWords.SMART_ORDER);
153            final Name.ExhibitFull r6[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
154                l6, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
155            assertTrue("result should not be null", r6 != null);
156            assertTrue("on input list with one different exhibit should yield one result", r6.length == 1);
157            assertTrue("on input list with one different exhibit should yield that exhibit", TextUtils.contentEquals(EXHIBIT2, r6[0]));
158    
159            // Add a second exhibit with the same main word,
160            // so both should now appear in the results...
161            final Name.ExhibitFull EXHIBIT3 = Name.ExhibitFull.create("m/m-www-MM.jpg");
162            exhibits1.put(EXHIBIT3, new ExhibitStaticAttr(EXHIBIT3, 100, now));
163    //        final AllExhibitProperties aep7 = new AllExhibitProperties(
164    //                                new ExhibitPropsGlobalImmutable(),
165    //                                new LocationMap(),
166    //                                new AllExhibitImmutableData(exhibits1, now),
167    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
168            final List<Name.ExhibitFull> l7 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
169            Collections.sort(l7, attrWords.SMART_ORDER);
170            final Name.ExhibitFull r7[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
171                l7, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
172            assertTrue("result should not be null", r7 != null);
173            assertTrue("on input list with two extra exhibits should yield two results", r7.length == 2);
174            assertTrue("both new exhibits should be in the results",
175                            TextUtils.contentEquals(EXHIBIT3, r7[0]) &&
176                            TextUtils.contentEquals(EXHIBIT2, r7[1]));
177    
178            // Now limit the output size and check that all outputs are
179            // the new exhibits.
180            final Name.ExhibitFull r7b[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
181                l7, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), 1);
182            assertTrue("result should not be null", r7b != null);
183            assertTrue("output should be limited to requested length", r7b.length == 1);
184            assertTrue("both new exhibits should be in the results",
185                    TextUtils.contentEquals(EXHIBIT3, r7b[0]) ||
186                    TextUtils.contentEquals(EXHIBIT2, r7b[0]));
187    
188            // Add a third exhibit with the same main word (but different in case),
189            // so both all should now appear in the results
190            // (comparator must be case-insensitive other than to break ties)...
191            final Name.ExhibitFull EXHIBIT4 = Name.ExhibitFull.create("m/M-vvv-MM.jpg");
192            exhibits1.put(EXHIBIT4, new ExhibitStaticAttr(EXHIBIT4, 100, now));
193    //        final AllExhibitProperties aep8 = new AllExhibitProperties(
194    //                                new ExhibitPropsGlobalImmutable(),
195    //                                new LocationMap(),
196    //                                new AllExhibitImmutableData(exhibits1, now),
197    //                                Collections.EMPTY_MAP /* Map _loadedProps */);
198            final List<Name.ExhibitFull> l8 = new Vector<Name.ExhibitFull>(exhibits1.keySet());
199            Collections.sort(l8, attrWords.SMART_ORDER);
200            final Name.ExhibitFull r8[] = SimpleSimilarExhibitFinder.getSimpleSimilarExhibitList(
201                l8, attrWords.SMART_ORDER, THIS_EXNAME, attrWords.getAttrWordsSortedSet(), Integer.MAX_VALUE);
202            assertTrue("result should not be null", r8 != null);
203            assertTrue("expect comparator to be case-insensitive", r8.length == 3);
204            assertTrue("all new exhibits should be in the results",
205                    TextUtils.contentEquals(EXHIBIT4, r8[0]) &&
206                    TextUtils.contentEquals(EXHIBIT3, r8[1]) &&
207                    TextUtils.contentEquals(EXHIBIT2, r8[2]));
208            }
209    
210        /**Test of generation of short exhibit name prefix for titles.
211         * The prefixes generated must always be valid non-zero-length
212         * whole-word prefixes of the file components of the exhibit names
213         * selected, that make sense ti a human reader,
214         * but need not be totally perfect.
215         */
216        public void testMinimalUniqueENTitlePrefix()
217            {
218            // Test that a zero-length List always returns an empty string,
219            // even for an otherwise-invalid index.
220            final List<Name.ExhibitFull> emptyExhibitsList = Collections.emptyList();
221            assertTrue("On empty list, result should be harmless empty string.",
222                       0 == WebUtils.minimalUniqueENTitlePrefix(emptyExhibitsList, -1).length());
223    
224            // Our set of fake input names.
225            final List<Name.ExhibitFull> sampleNames = Arrays.asList(new Name.ExhibitFull[]
226                {
227                Name.ExhibitFull.create("zzz/apple-DHD.jpg"),
228                Name.ExhibitFull.create("yyy/apple-turnover-ANON.gif"),
229                Name.ExhibitFull.create("yyy/Apple-turnover-2-ANON.gif"),
230                Name.ExhibitFull.create("aaa-bbb/cat-1-ANON.gif"),
231                Name.ExhibitFull.create("ccc_ddd/cat-2-ANON.gif"),
232                Name.ExhibitFull.create("ccc-ddd/dog-2-ANON.gif"),
233                Name.ExhibitFull.create("ccc-ddd/dog-in-manger-2-ANON.gif"),
234                Name.ExhibitFull.create("ccc-ddd/dog-zeal-2-ANON.gif"),
235                Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-market-1-MB.jpg"),
236                Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-1-MB.jpg"),
237                Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-2-MB.jpg"),
238                Name.ExhibitFull.create("places-and-sights/_more1999/_more07/Indonesia-Bali-Ubud-Monkey-Forest-Pura-Dalem-ie-Temple-of-The-Dead-3-MB.jpg"),
239                Name.ExhibitFull.create("aaa/zebra-in-zoo-ANON.png")
240                });
241    
242            // Expected abbreviation for each name.
243            final String expectedResponses[] =
244                {
245                "apple-",
246                "apple-turnover-",
247                "Apple-turnover-",
248                "cat-",
249                "cat-",
250                "dog-",
251                "dog-in-",
252                "dog-zeal-",
253                "Indonesia-Bali-Ubud-Monkey-Forest-market-",
254                "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
255                "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
256                "Indonesia-Bali-Ubud-Monkey-Forest-Pura-",
257                "zebra-"
258                };
259    
260            // Make sure that our test data is self-consistent.
261            assertTrue("test set wrong size", expectedResponses.length == sampleNames.size());
262    
263            // Test for friendly behaviour on bad inputs.
264            assertTrue("On negative (illegal) index, result should be harmless empty string.",
265                       0 == WebUtils.minimalUniqueENTitlePrefix(sampleNames, -11111).length());
266            assertTrue("On over-large (illegal) index, result should be harmless empty string.",
267                       0 == WebUtils.minimalUniqueENTitlePrefix(sampleNames, 3 + 2*sampleNames.size()).length());
268    
269    
270            // Test that we get the expected output for each index.
271            for(int i = sampleNames.size(); --i >= 0; )
272                {
273                final String actualResponse = WebUtils.minimalUniqueENTitlePrefix(sampleNames, i).toString();
274                assertTrue("unexpected response `"+actualResponse+"' at index "+i+
275                                " instead of `"+(expectedResponses[i])+"'",
276                           expectedResponses[i].equals(actualResponse));
277                }
278            }
279    
280        /**Simple tests of WebUtils.sanitiseForXML().
281         * This tests for robust behaviour of this routine with and without
282         * special handling of i18n marked-up text.
283         */
284        public static final void testSimple_sanitiseForXML()
285            {
286            // Maximum length for sanitisation.
287            final int maxLen = 16;
288            // This data has three columns:
289            //  1) The input String.
290            //  2) The output String if entity codes are not allowed.
291            //  3) The output String if entity codes are allowed.
292            final String testData[][] =
293                {
294                    // Empty string; no conversion needed.
295                    { "", "", "" },
296    
297                    // Convert null input to empty-string result.
298                    { null, "", "" },
299    
300                    // All whitespace; result is empty string.
301                    { " \r \n \r\n \t\t", "", "" },
302    
303                    // Simple short pure alphanumeric: no transform needed.
304                    { "abc1234Z", "abc1234Z", "abc1234Z" },
305    
306                    // Simple short text with whitespace: whitespace is trimmed.
307                    { " hot  dog!  ", "hot  dog!", "hot  dog!" },
308    
309                    // Long string is trimmed then truncated.
310                    { " a  long long string  ", "a  long long ...", "a  long long ..." },
311    
312                    // Test handling of a single entity.
313                    { "Cow &amp; Gate", "Cow +amp; Gate", "Cow &amp; Gate" },
314    
315                    // Test handling of a single entity that seems to make the string too long.
316                    { "&longbutvalidentitycode;", "+longbutvalid...", "&longbutvalidentitycode;" },
317    
318                    // Test handling of non-ASCII characters...
319                    { "Mer\u0127ba ghal", "Mer ba ghal", "Mer&#295;ba ghal" },
320                };
321    
322            for(int i = testData.length; --i >= 0; )
323                {
324                final String input = testData[i][0];
325                final String expectedNEOutput = testData[i][1];
326                final String expectedEOutput = testData[i][2];
327    
328                final String nEOutput = TextUtils.sanitiseForXML(input, maxLen, false);
329                assertTrue("no-entity-code output should be strictly length bounded",
330                           nEOutput.length() <= maxLen);
331                assertTrue("unexpected no-entity-code result at index "+i+
332                           " got `"+nEOutput+"', expected `"+expectedNEOutput+"'",
333                   expectedNEOutput.equals(nEOutput));
334    
335                final String eOutput = TextUtils.sanitiseForXML(input, maxLen, true);
336                assertTrue("unexpected entity-code result at index "+i+
337                           " got `"+eOutput+"', expected `"+expectedEOutput+"'",
338                   expectedEOutput.equals(eOutput));
339                }
340            }
341    
342        /**Test bot/spider matching from UA name. */
343        public static void testUABotNameMatching()
344            {
345            if(WebUtils.UA_REGEX == null)
346                {
347                Main.getOut().println("UA_REGEX is null; skipping test...");
348                return;
349                }
350    
351            assertFalse("Must not class vanilla browser (eg FF) as a bot", WebUtils.UA_REGEX.matcher("Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7").matches());
352            assertTrue("Must catch common bots (eg IA)",  WebUtils.UA_REGEX.matcher("ia_archiver").matches());
353            assertTrue("Should match newly-created bot name",  WebUtils.UA_REGEX.matcher("MyNewBadBot").matches());
354            }
355    
356        private static final String TEST_STRONG_ETAG_VALUE = "\"123\"";
357        private static final String TEST_WEAK_ETAG_VALUE = "W/\"abc\"";
358    
359        /**Test very basic If-None-Match / If-Modified-Since handling. */
360        public static void testINMIMSBasics()
361            {
362            // Check that request/response args are not even examined or used with LM/ETag unknown
363            // and the return is always false (don't abort, send the body).
364            assertFalse("with last modified == -1 no other args are even checked",
365                    WebUtils.abortIfNotModifiedSince(-1, null, null));
366            assertFalse("with eTag == null and last modified == -1 no other args are even checked",
367                    WebUtils.abortIfETagMatchOrNotModifiedSince(null, -1, null, null));
368            assertFalse("with last modified == -1 no other args are even checked",
369                    WebUtils.abortIfNotModifiedSince(-1, new BaseMockHttpServletRequest(){}, new BaseMockHttpServletResponse(){}));
370            assertFalse("with eTag == null and last modified == -1 no other args are even checked",
371                    WebUtils.abortIfETagMatchOrNotModifiedSince(null, -1, new BaseMockHttpServletRequest(){}, new BaseMockHttpServletResponse(){}));
372    
373            // Check that with no If-Modified-Since or If-None-Match set
374            // that the return is always false (don't abort, send the body)
375            // for any reasonable/positive Last-Modified / ETag.
376            final HttpServletRequest noParamsRequest = new MockHttpServletRequestWithParams(Collections.<String,String>emptyMap());
377            assertFalse("with last modified == -1 no other args are even checked",
378                    WebUtils.abortIfNotModifiedSince(System.currentTimeMillis(),
379                            noParamsRequest,
380                            new BaseMockHttpServletResponse()));
381            assertFalse("with eTag == null and last modified == -1 no other args are even checked",
382                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, System.currentTimeMillis(),
383                            noParamsRequest,
384                            new BaseMockHttpServletResponse()));
385            }
386    
387        /**Mock response that accepts setting of status value. */
388        static final class MockHttpServletResponseThatAcceptsStatus extends BaseMockHttpServletResponse
389            {
390            /**Status value; null until set. */
391            Integer status;
392    
393            @Override public void setStatus(final int arg0)
394                { status = Integer.valueOf(arg0); }
395            }
396    
397        /**Test If-Modified-Since handling. */
398        public static void testIMS()
399            {
400            // Check that Last-Modified / If-Modified-Since behaves as expected.
401            // Create a time for the If-Modified-Since header a little in the past.
402            final long tIMS = System.currentTimeMillis() - 50000;
403            final SimpleDateFormat formatter = new SimpleDateFormat(MockHttpServletRequestWithParams.DATE_PATTERN_RFC1123, Locale.US);
404            formatter.setTimeZone(MockHttpServletRequestWithParams.GMT);
405            final String headerValueIMS = formatter.format(new Date(tIMS));
406            final HttpServletRequest request = new MockHttpServletRequestWithParams(Collections.singletonMap("If-Modified-Since", headerValueIMS));
407    
408            // Check that if last-modified time is at least 1s before the IMS header,
409            // then the body should not be generated (ie client's current copy is up-to-date).
410            final MockHttpServletResponseThatAcceptsStatus response1 = new MockHttpServletResponseThatAcceptsStatus();
411            assertTrue("with last modified 1s before IMS, then body should not be sent",
412                    WebUtils.abortIfNotModifiedSince(tIMS - 1000, request, response1));
413            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response1.status));
414    
415            // Check that if last-modified time is the same as the the IMS header,
416            // then the body should not be generated (ie client's current copy is up-to-date).
417            final MockHttpServletResponseThatAcceptsStatus response2 = new MockHttpServletResponseThatAcceptsStatus();
418            assertTrue("with last modified is same as IMS, then body should not be sent",
419                    WebUtils.abortIfNotModifiedSince(tIMS, request, response2));
420            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response2.status));
421    
422            // Check that if last-modified time is at least 1s after the IMS header,
423            // then the body should be generated (ie client's current copy is not up-to-date).
424            final MockHttpServletResponseThatAcceptsStatus response3 = new MockHttpServletResponseThatAcceptsStatus();
425            assertFalse("with last modified 1s after as IMS, then body should be sent",
426                    WebUtils.abortIfNotModifiedSince(tIMS + 1000, request, response3));
427            assertNull("correct stats should have been set", response3.status);
428            }
429    
430        /**Test If-None-Match handling. */
431        public static void testINM()
432            {
433            // Check that ETag / If-None-Match behaves as expected.
434    
435            // Check that an If-None-Match header of '*' always results in true (abort) when we have an ETag.
436            final HttpServletRequest request1 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", "*"));
437            final MockHttpServletResponseThatAcceptsStatus response1 = new MockHttpServletResponseThatAcceptsStatus();
438            assertTrue("with eTag != null (and last modified == -1), If-None-Match: * should force abort",
439                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, -1, request1, response1));
440            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response1.status));
441    
442            // Basic behaviour of underlying library support...
443            final BasicHeader bh1 = new BasicHeader("If-None-Match", "*");
444            assertEquals(1, bh1.getElements().length);
445            assertEquals("*", bh1.getElements()[0].toString());
446            final BasicHeader bh2 = new BasicHeader("If-None-Match", TEST_STRONG_ETAG_VALUE);
447            assertEquals(1, bh2.getElements().length);
448            assertEquals(TEST_STRONG_ETAG_VALUE, bh2.getElements()[0].toString());
449            final BasicHeader bh3 = new BasicHeader("If-None-Match", TEST_STRONG_ETAG_VALUE + ", " + TEST_WEAK_ETAG_VALUE);
450            assertEquals(2, bh3.getElements().length);
451            assertEquals(TEST_STRONG_ETAG_VALUE, bh3.getElements()[0].toString());
452            assertEquals(TEST_WEAK_ETAG_VALUE, bh3.getElements()[1].toString());
453    
454            // Check that matching ETag results in true (abort).
455            final HttpServletRequest request2 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
456            final MockHttpServletResponseThatAcceptsStatus response2 = new MockHttpServletResponseThatAcceptsStatus();
457            assertTrue("with eTag set (and last modified == -1), If-None-Match: eTag should force abort",
458                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, -1, request2, response2));
459            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response2.status));
460            final HttpServletRequest request3 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_STRONG_ETAG_VALUE + ", " + TEST_WEAK_ETAG_VALUE));
461            final MockHttpServletResponseThatAcceptsStatus response3 = new MockHttpServletResponseThatAcceptsStatus();
462            assertTrue("with eTag set (and last modified == -1), If-None-Match: eTag should force abort",
463                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, -1, request3, response3));
464            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response3.status));
465    
466            // Check that IMS ignored if INM is present.
467            // Follow ETag (abort) even if LM would not abort.
468            final long tIMS = System.currentTimeMillis() - 50000;
469            final SimpleDateFormat formatter = new SimpleDateFormat(MockHttpServletRequestWithParams.DATE_PATTERN_RFC1123, Locale.US);
470            formatter.setTimeZone(MockHttpServletRequestWithParams.GMT);
471            final String headerValueIMS = formatter.format(new Date(tIMS));
472            final Map<String, String> headers = new HashMap<String, String>();
473            headers.put("If-None-Match", TEST_WEAK_ETAG_VALUE);
474            headers.put("If-Modified-Since", headerValueIMS);
475            final HttpServletRequest request4 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
476            final MockHttpServletResponseThatAcceptsStatus response4 = new MockHttpServletResponseThatAcceptsStatus();
477            assertTrue("with eTag set (even with last modified newer), If-None-Match: eTag should force abort",
478                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_WEAK_ETAG_VALUE, tIMS + 1000, request4, response4));
479            assertTrue("correct stats should have been set", Integer.valueOf(HttpServletResponse.SC_NOT_MODIFIED).equals(response4.status));
480            // Follow ETag (not abort) even if LM would abort.
481            headers.put("If-None-Match", TEST_WEAK_ETAG_VALUE);
482            headers.put("If-Modified-Since", headerValueIMS);
483            final HttpServletRequest request5 = new MockHttpServletRequestWithParams(Collections.singletonMap("If-None-Match", TEST_WEAK_ETAG_VALUE));
484            final MockHttpServletResponseThatAcceptsStatus response5 = new MockHttpServletResponseThatAcceptsStatus();
485            assertFalse("with eTag set (even with last modified newer), If-None-Match: eTag should force abort",
486                    WebUtils.abortIfETagMatchOrNotModifiedSince(TEST_STRONG_ETAG_VALUE, tIMS - 1000, request5, response5));
487            assertNull("stats should not have been set", response5.status);
488            }
489    
490        /**Test our sanitisation of referrer names... */
491        public static final void testReferrerHostNameSanitisation()
492            {
493            final String benign = "my.host.com";
494            assertEquals("benign lower-case name should be left alone", benign, ServletUtils.sanitiseReferrerHostName(benign));
495            final String ucBenign = "UC.host.com";
496            assertEquals("benign (part) upper-case name should be lower-cased", ucBenign.toLowerCase(), ServletUtils.sanitiseReferrerHostName(ucBenign));
497            final String badChars = "$\u0fff\n%";
498            assertEquals("bad chars should be replaced with ?", "????", ServletUtils.sanitiseReferrerHostName(badChars));
499            final String veryLong = "abcdefghijklmnopqrstuvwxyz.tediously.long.DOM.ain";
500            final String sVeryLong = ServletUtils.sanitiseReferrerHostName(veryLong);
501            assertTrue("very long domain should be truncated on left", sVeryLong.endsWith(".dom.ain"));
502            assertTrue("very long domain should start with '.' to indicate truncation", sVeryLong.startsWith("."));
503            assertTrue("very long domain should sanitised to lower-cased substring", veryLong.toLowerCase().endsWith(sVeryLong));
504            assertTrue("long IPv6 literal not truncated on left", ServletUtils.sanitiseReferrerHostName("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]").startsWith("[fedc:"));
505            assertEquals("short IPv6 literal with embedded IPv4 not truncated", ServletUtils.sanitiseReferrerHostName("[::192.9.5.5]"), "[::192.9.5.5]");
506            }
507        }