Paths.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015-2016 ForgeRock AS.
 */

package org.forgerock.http.util;

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Utilities for manipulating paths.
 */
public final class Paths {
    private static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/");

    /**
     * Returns the URL path decoding of the provided object's string
     * representation.
     *
     * @param value
     *            The value to be URL path decoded.
     * @return The URL path decoding of the provided object's string
     *         representation.
     */
    public static String urlDecode(final Object value) {
        return Uris.urlDecodePathElement(value.toString());
    }

    /**
     * Returns the URL path encoding of the provided object's string
     * representation.
     *
     * @param value
     *            The value to be URL path encoded.
     * @return The URL path encoding of the provided object's string
     *         representation.
     */
    public static String urlEncode(final Object value) {
        return Uris.urlEncodePathElement(value.toString());
    }

    /**
     * Converts a path into a list of URL-decoded path elements. If the leading path element
     * is empty it is dropped, meaning that {@code null}, {@code ""} and {@code "/"} will
     * all return an empty list, and {@code "//"} will return a list with two elements, both
     * empty strings, as all intermediate and trailing empty paths are retained.
     *
     * @param rawPath The raw, URL-encoded path string.
     * @return An immutable list of the path elements.
     */
    public static List<String> getPathElements(String rawPath) {
        String[] pathElements = null;
        if (rawPath != null) {
            if (rawPath.startsWith("/")) {
                rawPath = rawPath.substring(1);
            }
            pathElements = PATH_SPLIT_PATTERN.split(rawPath, -1);
            if (pathElements.length == 1 && pathElements[0].isEmpty()) {
                pathElements = null;
            }
        }

        List<String> elements;
        if (pathElements == null) {
            elements = emptyList();
        } else {
            List<String> decodedElements = new ArrayList<>(pathElements.length);
            for (String element : pathElements) {
                decodedElements.add(Paths.urlDecode(element));
            }
            elements = decodedElements;
        }
        return unmodifiableList(elements);
    }

    /**
     * Joins a list of URL-decoded path elements into a url-encoded path.
     * @param elements The list of (URL-decoded) elements.
     * @return The raw path.
     */
    public static String joinPath(List<String> elements) {
        if (elements == null) {
            return "";
        }
        StringBuilder s = new StringBuilder();
        for (String element : elements) {
            if (s.length() > 0) {
                s.append("/");
            }
            s.append(urlEncode(element));
        }
        return s.toString();
    }

    /**
     * Removes trailing slash (if there is any), returns the same value otherwise.
     * Note that this method does not recursively clean the value from all its trailing slashes ({@literal /openam//}
     * will be transformed to {@literal /openam/}, not {@literal /openam}).
     *
     * @param path path with (possibly) trailing slash
     * @return path without the trailing slash (if there was one), same path otherwise
     */
    public static String removeTrailingSlash(String path) {
        return path.endsWith("/")
                ? path.substring(0, path.length() - 1)
                : path;
    }

    /**
     * Add leading slash (if there is not already), returns the same value otherwise.
     *
     * @param path path without (possibly) leading slash
     * @return path with a leading slash (if there was none), same path otherwise
     */
    public static String addLeadingSlash(String path) {
        return path.startsWith("/")
                ? path
                : "/" + path;
    }

    private Paths() {
        // utilities only.
    }
}