/*
 *  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.catalina.filters;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Calendar;
import java.util.List;
import java.util.Map.Entry;
import java.util.StringTokenizer;
import java.util.TimeZone;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.junit.Assert;

import org.junit.Test;

import org.apache.catalina.Context;
import org.apache.catalina.filters.ExpiresFilter.Duration;
import org.apache.catalina.filters.ExpiresFilter.DurationUnit;
import org.apache.catalina.filters.ExpiresFilter.ExpiresConfiguration;
import org.apache.catalina.filters.ExpiresFilter.StartingPoint;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;

public class TestExpiresFilter extends TomcatBaseTest {
    public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");

    @Test
    public void testConfiguration() throws Exception {

        Tomcat tomcat = getTomcatInstance();
        Context root = tomcat.addContext("", TEMP_DIR);

        FilterDef filterDef = new FilterDef();
        filterDef.addInitParameter("ExpiresDefault", "access plus 1 month");
        filterDef.addInitParameter("ExpiresByType text/html",
                "access plus 1 month 15 days 2 hours");
        filterDef.addInitParameter("ExpiresByType image/gif",
                "modification plus 5 hours 3 minutes");
        filterDef.addInitParameter("ExpiresByType image/jpg", "A10000");
        filterDef.addInitParameter("ExpiresByType video/mpeg", "M20000");
        filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes",
                "304, 503");

        ExpiresFilter expiresFilter = new ExpiresFilter();

        filterDef.setFilter(expiresFilter);
        filterDef.setFilterClass(ExpiresFilter.class.getName());
        filterDef.setFilterName(ExpiresFilter.class.getName());

        root.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(ExpiresFilter.class.getName());
        filterMap.addURLPatternDecoded("*");

        tomcat.start();
        try {
            // VERIFY EXCLUDED RESPONSE STATUS CODES
            int[] excludedResponseStatusCodes = expiresFilter.getExcludedResponseStatusCodesAsInts();
            Assert.assertEquals(2, excludedResponseStatusCodes.length);
            Assert.assertEquals(304, excludedResponseStatusCodes[0]);
            Assert.assertEquals(503, excludedResponseStatusCodes[1]);

            // VERIFY DEFAULT CONFIGURATION
            ExpiresConfiguration expiresConfigurationDefault =
                    expiresFilter.getDefaultExpiresConfiguration();
            Assert.assertEquals(StartingPoint.ACCESS_TIME,
                    expiresConfigurationDefault.getStartingPoint());
            Assert.assertEquals(1, expiresConfigurationDefault.getDurations().size());
            Assert.assertEquals(DurationUnit.MONTH,
                    expiresConfigurationDefault.getDurations().get(0).getUnit());
            Assert.assertEquals(1, expiresConfigurationDefault.getDurations().get(0).getAmount());

            // VERIFY TEXT/HTML
            ExpiresConfiguration expiresConfigurationTextHtml =
                    expiresFilter.getExpiresConfigurationByContentType().get("text/html");
            Assert.assertEquals(StartingPoint.ACCESS_TIME,
                    expiresConfigurationTextHtml.getStartingPoint());

            Assert.assertEquals(3, expiresConfigurationTextHtml.getDurations().size());

            Duration oneMonth = expiresConfigurationTextHtml.getDurations().get(0);
            Assert.assertEquals(DurationUnit.MONTH, oneMonth.getUnit());
            Assert.assertEquals(1, oneMonth.getAmount());

            Duration fifteenDays = expiresConfigurationTextHtml.getDurations().get(1);
            Assert.assertEquals(DurationUnit.DAY, fifteenDays.getUnit());
            Assert.assertEquals(15, fifteenDays.getAmount());

            Duration twoHours = expiresConfigurationTextHtml.getDurations().get(2);
            Assert.assertEquals(DurationUnit.HOUR, twoHours.getUnit());
            Assert.assertEquals(2, twoHours.getAmount());

            // VERIFY IMAGE/GIF
            ExpiresConfiguration expiresConfigurationImageGif =
                    expiresFilter.getExpiresConfigurationByContentType().get("image/gif");
            Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME,
                    expiresConfigurationImageGif.getStartingPoint());

            Assert.assertEquals(2, expiresConfigurationImageGif.getDurations().size());

            Duration fiveHours = expiresConfigurationImageGif.getDurations().get(0);
            Assert.assertEquals(DurationUnit.HOUR, fiveHours.getUnit());
            Assert.assertEquals(5, fiveHours.getAmount());

            Duration threeMinutes = expiresConfigurationImageGif.getDurations().get(1);
            Assert.assertEquals(DurationUnit.MINUTE, threeMinutes.getUnit());
            Assert.assertEquals(3, threeMinutes.getAmount());

            // VERIFY IMAGE/JPG
            ExpiresConfiguration expiresConfigurationImageJpg =
                    expiresFilter.getExpiresConfigurationByContentType().get("image/jpg");
            Assert.assertEquals(StartingPoint.ACCESS_TIME,
                    expiresConfigurationImageJpg.getStartingPoint());

            Assert.assertEquals(1, expiresConfigurationImageJpg.getDurations().size());

            Duration tenThousandSeconds = expiresConfigurationImageJpg.getDurations().get(0);
            Assert.assertEquals(DurationUnit.SECOND, tenThousandSeconds.getUnit());
            Assert.assertEquals(10000, tenThousandSeconds.getAmount());

            // VERIFY VIDEO/MPEG
            ExpiresConfiguration expiresConfiguration =
                    expiresFilter.getExpiresConfigurationByContentType().get("video/mpeg");
            Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME,
                    expiresConfiguration.getStartingPoint());

            Assert.assertEquals(1, expiresConfiguration.getDurations().size());

            Duration twentyThousandSeconds = expiresConfiguration.getDurations().get(0);
            Assert.assertEquals(DurationUnit.SECOND, twentyThousandSeconds.getUnit());
            Assert.assertEquals(20000, twentyThousandSeconds.getAmount());
        } finally {
            tomcat.stop();
        }
    }

    /*
     * Test that a resource with empty content is also processed
     */
    @Test
    public void testEmptyContent() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/plain");
                // no content is written in the response
            }
        };

        validate(servlet, Integer.valueOf(7 * 60));
    }

    @Test
    public void testParseExpiresConfigurationCombinedDuration() {
        ExpiresFilter expiresFilter = new ExpiresFilter();
        ExpiresConfiguration actualConfiguration = expiresFilter.parseExpiresConfiguration("access plus 1 month 15 days 2 hours");

        Assert.assertEquals(StartingPoint.ACCESS_TIME,
                actualConfiguration.getStartingPoint());

        Assert.assertEquals(3, actualConfiguration.getDurations().size());

    }

    @Test
    public void testParseExpiresConfigurationMonoDuration() {
        ExpiresFilter expiresFilter = new ExpiresFilter();
        ExpiresConfiguration actualConfiguration = expiresFilter.parseExpiresConfiguration("access plus 2 hours");

        Assert.assertEquals(StartingPoint.ACCESS_TIME,
                actualConfiguration.getStartingPoint());

        Assert.assertEquals(1, actualConfiguration.getDurations().size());
        Assert.assertEquals(2,
                actualConfiguration.getDurations().get(0).getAmount());
        Assert.assertEquals(DurationUnit.HOUR,
                actualConfiguration.getDurations().get(0).getUnit());

    }

    @Test
    public void testSkipBecauseCacheControlMaxAgeIsDefined() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/xml; charset=utf-8");
                response.addHeader("Cache-Control", "private, max-age=232");
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(232));
    }

    @Test
    public void testExcludedResponseStatusCode() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                response.addHeader("ETag", "W/\"1934-1269208821000\"");
                response.addDateHeader("Date", System.currentTimeMillis());
            }
        };

        validate(servlet, null, HttpServletResponse.SC_NOT_MODIFIED);
    }

    @Test
    public void testNullContentType() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType(null);
            }
        };

        validate(servlet, Integer.valueOf(1 * 60));
    }

    @Test
    public void testSkipBecauseExpiresIsDefined() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/xml; charset=utf-8");
                response.addDateHeader("Expires", System.currentTimeMillis());
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, null);
    }

    @Test
    public void testUseContentTypeExpiresConfiguration() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/xml; charset=utf-8");
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(3 * 60));
    }

    @Test
    public void testUseContentTypeWithoutCharsetExpiresConfiguration()
            throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/xml; charset=iso-8859-1");
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(5 * 60));
    }

    @Test
    public void testUseDefaultConfiguration1() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("image/jpeg");
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(1 * 60));
    }

    @Test
    public void testUseDefaultConfiguration2() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("image/jpeg");
                response.addHeader("Cache-Control", "private");

                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(1 * 60));
    }

    @Test
    public void testUseMajorTypeExpiresConfiguration() throws Exception {
        HttpServlet servlet = new HttpServlet() {
            private static final long serialVersionUID = 1L;

            @Override
            protected void service(HttpServletRequest request,
                    HttpServletResponse response) throws ServletException,
                    IOException {
                response.setContentType("text/json; charset=iso-8859-1");
                response.getWriter().print("Hello world");
            }
        };

        validate(servlet, Integer.valueOf(7 * 60));
    }

    protected void validate(HttpServlet servlet, Integer expectedMaxAgeInSeconds)
            throws Exception {
        validate(servlet, expectedMaxAgeInSeconds, HttpURLConnection.HTTP_OK);
    }

    protected void validate(HttpServlet servlet,
            Integer expectedMaxAgeInSeconds, int expectedResponseStatusCode)
            throws Exception {
        // SETUP

        Tomcat tomcat = getTomcatInstance();
        Context root = tomcat.addContext("", TEMP_DIR);

        FilterDef filterDef = new FilterDef();
        filterDef.addInitParameter("ExpiresDefault", "access plus 1 minute");
        filterDef.addInitParameter("ExpiresByType text/xml;charset=utf-8",
                "access plus 3 minutes");
        filterDef.addInitParameter("ExpiresByType text/xml",
                "access plus 5 minutes");
        filterDef.addInitParameter("ExpiresByType text",
                "access plus 7 minutes");
        filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes",
                "304, 503");

        filterDef.setFilterClass(ExpiresFilter.class.getName());
        filterDef.setFilterName(ExpiresFilter.class.getName());

        root.addFilterDef(filterDef);

        FilterMap filterMap = new FilterMap();
        filterMap.setFilterName(ExpiresFilter.class.getName());
        filterMap.addURLPatternDecoded("*");
        root.addFilterMap(filterMap);

        Tomcat.addServlet(root, servlet.getClass().getName(), servlet);
        root.addServletMappingDecoded("/test", servlet.getClass().getName());

        tomcat.start();

        try {
            Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            long timeBeforeInMillis = System.currentTimeMillis();

            // TEST
            HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(
                    "http://localhost:" + tomcat.getConnector().getLocalPort() +
                            "/test").openConnection();

            // VALIDATE
            Assert.assertEquals(expectedResponseStatusCode,
                    httpURLConnection.getResponseCode());

            StringBuilder msg = new StringBuilder();
            for (Entry<String, List<String>> field : httpURLConnection.getHeaderFields().entrySet()) {
                for (String value : field.getValue()) {
                    msg.append((field.getKey() == null ? "" : field.getKey() +
                            ": ") +
                            value + "\n");
                }
            }
            System.out.println(msg);

            Integer actualMaxAgeInSeconds;

            String cacheControlHeader = httpURLConnection.getHeaderField("Cache-Control");
            if (cacheControlHeader == null) {
                actualMaxAgeInSeconds = null;
            } else {
                actualMaxAgeInSeconds = null;
                StringTokenizer cacheControlTokenizer = new StringTokenizer(
                        cacheControlHeader, ",");
                while (cacheControlTokenizer.hasMoreTokens() &&
                        actualMaxAgeInSeconds == null) {
                    String cacheDirective = cacheControlTokenizer.nextToken();
                    StringTokenizer cacheDirectiveTokenizer = new StringTokenizer(
                            cacheDirective, "=");
                    if (cacheDirectiveTokenizer.countTokens() == 2) {
                        String key = cacheDirectiveTokenizer.nextToken().trim();
                        String value = cacheDirectiveTokenizer.nextToken().trim();
                        if (key.equalsIgnoreCase("max-age")) {
                            actualMaxAgeInSeconds = Integer.valueOf(value);
                        }
                    }
                }
            }

            if (expectedMaxAgeInSeconds == null) {
                Assert.assertNull("actualMaxAgeInSeconds '" +
                        actualMaxAgeInSeconds + "' should be null",
                        actualMaxAgeInSeconds);
                return;
            }

            Assert.assertNotNull(actualMaxAgeInSeconds);

            int deltaInSeconds = Math.abs(actualMaxAgeInSeconds.intValue() -
                    expectedMaxAgeInSeconds.intValue());
            Assert.assertTrue("actualMaxAgeInSeconds: " +
                    actualMaxAgeInSeconds + ", expectedMaxAgeInSeconds: " +
                    expectedMaxAgeInSeconds + ", request time: " +
                    timeBeforeInMillis + " for content type " +
                    httpURLConnection.getContentType(), deltaInSeconds < 3);

        } finally {
            tomcat.stop();
        }
    }

    @Test
    public void testIntsToCommaDelimitedString() {
        String actual = ExpiresFilter.intsToCommaDelimitedString(new int[] {
                500, 503 });
        String expected = "500, 503";

        Assert.assertEquals(expected, actual);
    }
}
