RedirectBuilder.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed 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 io.undertow.util;

import io.undertow.server.HttpServerExchange;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Map;

/**
 * Utility class for building redirects.
 *
 * @author Stuart Douglas
 */
public class RedirectBuilder {

    public static final String UTF_8 = StandardCharsets.UTF_8.name();

    /**
     * Redirects to a new relative path. All other data from the exchange is preserved.
     *
     * @param exchange        The HTTP server exchange
     * @param newRelativePath The new relative path
     * @return
     */
    public static String redirect(final HttpServerExchange exchange, final String newRelativePath) {
        return redirect(exchange, newRelativePath, true);
    }

    /**
     * Redirects to a new relative path. All other data from the exchange is preserved.
     *
     * @param exchange          The HTTP server exchange
     * @param newRelativePath   The new relative path
     * @param includeParameters If query and path parameters from the exchange should be included
     * @return
     */
    public static String redirect(final HttpServerExchange exchange, final String newRelativePath, final boolean includeParameters) {
        try {
            StringBuilder uri = new StringBuilder(exchange.getRequestScheme());
            uri.append("://");
            uri.append(exchange.getHostAndPort());
            uri.append(encodeUrlPart(exchange.getResolvedPath()));
            if (exchange.getResolvedPath().endsWith("/")) {
                if (newRelativePath.startsWith("/")) {
                    uri.append(encodeUrlPart(newRelativePath.substring(1)));
                } else {
                    uri.append(encodeUrlPart(newRelativePath));
                }
            } else {
                if (!newRelativePath.startsWith("/")) {
                    uri.append('/');
                }
                uri.append(encodeUrlPart(newRelativePath));
            }
            if (includeParameters) {
                if (!exchange.getPathParameters().isEmpty()) {
                    boolean first = true;
                    uri.append(';');
                    for (Map.Entry<String, Deque<String>> param : exchange.getPathParameters().entrySet()) {
                        for (String value : param.getValue()) {
                            if (first) {
                                first = false;
                            } else {
                                uri.append('&');
                            }
                            uri.append(URLEncoder.encode(param.getKey(), UTF_8));
                            uri.append('=');
                            uri.append(URLEncoder.encode(value, UTF_8));
                        }
                    }
                }
                if (!exchange.getQueryString().isEmpty()) {
                    uri.append('?');
                    uri.append(exchange.getQueryString());
                }
            }
            return uri.toString();
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * perform URL encoding
     * <p>
     * TODO: this whole thing is kinda crappy.
     *
     * @return
     */
    private static String encodeUrlPart(final String part) throws UnsupportedEncodingException {
        //we need to go through and check part by part that a section does not need encoding

        int pos = 0;
        for (int i = 0; i < part.length(); ++i) {
            char c = part.charAt(i);
            if(c == '?') {
                break;
            } else if (c == '/') {
                if (pos != i) {
                    String original = part.substring(pos, i);
                    String encoded = URLEncoder.encode(original, UTF_8);
                    if (!encoded.equals(original)) {
                        return realEncode(part, pos);
                    }
                }
                pos = i + 1;
            } else if (c == ' ') {
                return realEncode(part, pos);
            }
        }
        return part;
    }

    private static String realEncode(String part, int startPos) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        sb.append(part.substring(0, startPos));
        int pos = startPos;
        for (int i = startPos; i < part.length(); ++i) {
            char c = part.charAt(i);
            if(c == '?') {
                break;
            } else if (c == '/') {
                if (pos != i) {
                    String original = part.substring(pos, i);
                    String encoded = URLEncoder.encode(original, UTF_8);
                    sb.append(encoded);
                    sb.append('/');
                    pos = i + 1;
                }
            }
        }

        String original = part.substring(pos);
        String encoded = URLEncoder.encode(original, UTF_8);
        sb.append(encoded);
        return sb.toString();
    }

    private RedirectBuilder() {

    }
}