ResponseUtil.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.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;

import jakarta.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.parser.TokenList;

public class ResponseUtil {

    private static final String VARY_HEADER = "vary";
    private static final String VARY_ALL = "*";

    private ResponseUtil() {
        // Utility class. Hide default constructor.
    }


    public static void addVaryFieldName(MimeHeaders headers, String name) {
        addVaryFieldName(new HeaderAdapter(headers), name);
    }


    public static void addVaryFieldName(HttpServletResponse response, String name) {
        addVaryFieldName(new ResponseAdapter(response), name);
    }


    private static void addVaryFieldName(Adapter adapter, String name) {

        Collection<String> varyHeaders = adapter.getHeaders(VARY_HEADER);

        // Short-cut if only * has been set
        if (varyHeaders.size() == 1 && varyHeaders.iterator().next().trim().equals(VARY_ALL)) {
            // No need to add an additional field
            return;
        }

        // Short-cut if no headers have been set
        if (varyHeaders.size() == 0) {
            adapter.addHeader(VARY_HEADER, name);
            return;
        }

        // Short-cut if "*" is added
        if (VARY_ALL.equals(name.trim())) {
            adapter.setHeader(VARY_HEADER, VARY_ALL);
            return;
        }

        // May be dealing with an application set header, or multiple headers.
        // Header names overlap so can't use String.contains(). Have to parse
        // the existing values, check if the new value is already present and
        // then add it if not. The good news is field names are tokens which
        // makes parsing simpler.
        LinkedHashSet<String> fieldNames = new LinkedHashSet<>();

        for (String varyHeader : varyHeaders) {
            StringReader input = new StringReader(varyHeader);
            try {
                TokenList.parseTokenList(input, fieldNames);
            } catch (IOException ioe) {
                // Should never happen
            }
        }

        if (fieldNames.contains(VARY_ALL)) {
            // '*' has been added without removing other values. Optimise.
            adapter.setHeader(VARY_HEADER, VARY_ALL);
            return;
        }

        // Build single header to replace current multiple headers
        // Replace existing header(s) to ensure any invalid values are removed
        fieldNames.add(name);
        StringBuilder varyHeader = new StringBuilder();
        Iterator<String> iter = fieldNames.iterator();
        // There must be at least one value as one is added just above
        varyHeader.append(iter.next());
        while (iter.hasNext()) {
            varyHeader.append(',');
            varyHeader.append(iter.next());
        }
        adapter.setHeader(VARY_HEADER, varyHeader.toString());
    }


    private interface Adapter {

        Collection<String> getHeaders(String name);

        void setHeader(String name, String value);

        void addHeader(String name, String value);
    }


    private static final class HeaderAdapter implements Adapter {
        private final MimeHeaders headers;

        HeaderAdapter(MimeHeaders headers) {
            this.headers = headers;
        }

        @Override
        public Collection<String> getHeaders(String name) {
            Enumeration<String> values = headers.values(name);
            List<String> result = new ArrayList<>();
            while (values.hasMoreElements()) {
                result.add(values.nextElement());
            }
            return result;
        }

        @Override
        public void setHeader(String name, String value) {
            headers.setValue(name).setString(value);
        }

        @Override
        public void addHeader(String name, String value) {
            headers.addValue(name).setString(value);
        }
    }


    private static final class ResponseAdapter implements Adapter {
        private final HttpServletResponse response;

        ResponseAdapter(HttpServletResponse response) {
            this.response = response;
        }

        @Override
        public Collection<String> getHeaders(String name) {
            return response.getHeaders(name);
        }

        @Override
        public void setHeader(String name, String value) {
            response.setHeader(name, value);
        }

        @Override
        public void addHeader(String name, String value) {
            response.addHeader(name, value);
        }
    }
}