001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016package org.forgerock.api.util;
017
018import static org.forgerock.api.util.ValidationUtil.isEmpty;
019
020import java.util.ArrayList;
021import java.util.List;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025import org.forgerock.api.enums.ParameterSource;
026import org.forgerock.api.models.Parameter;
027
028/** Utilities for working with API Description paths and path-parameters. */
029public final class PathUtil {
030
031    /** Pattern for replacing multiple forward-slashes with a single forward-slash. */
032    private static final Pattern SQUASH_FORWARD_SLASHES_PATTERN = Pattern.compile("[/]{2,}");
033    /** Pattern for removing multiple trailing-slashes. */
034    private static final Pattern TRAILING_SLASHES_PATTERN = Pattern.compile("[/]+$");
035    /** Pattern for finding curly-brace-delimited path-variables in a URL-path. */
036    private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile("\\{([^{}]+)\\}");
037
038    private PathUtil() {
039        // empty
040    }
041
042    /**
043     * Builds a forward-slash-delimited path, with duplicate forward-slashes removed, and trailing slashes removed.
044     *
045     * @param segment First path segment
046     * @param moreSegments Additional path segments or {@code null}
047     * @return Path
048     */
049    public static String buildPath(final String segment, final String... moreSegments) {
050        if (isEmpty(segment)) {
051            throw new IllegalArgumentException("segment argument required");
052        }
053        final StringBuilder path = new StringBuilder().append('/').append(segment);
054        if (moreSegments != null) {
055            for (final String s : moreSegments) {
056                path.append('/').append(s);
057            }
058        }
059
060        // squash forward-slashes
061        final Matcher m = SQUASH_FORWARD_SLASHES_PATTERN.matcher(path);
062        final String normalized = m.find() ? m.replaceAll("/") : path.toString();
063
064        // remove trailing-slashes
065        return TRAILING_SLASHES_PATTERN.matcher(normalized).replaceAll("");
066    }
067
068    /**
069     * Searches for curly-braces in the given {@code pathSegment}, and creates a path-parameter for each that are found.
070     *
071     * @param pathSegment Path-segment
072     * @return Path-parameters or {@code null}
073     */
074    public static Parameter[] buildPathParameters(final String pathSegment) {
075        if (!isEmpty(pathSegment)) {
076            final Matcher m = PATH_VARIABLE_PATTERN.matcher(pathSegment);
077            if (m.find()) {
078                final List<Parameter> parameters = new ArrayList<>();
079                int start = 0;
080                while (m.find(start)) {
081                    parameters.add(Parameter.parameter()
082                            .name(m.group(1))
083                            .type("string")
084                            .source(ParameterSource.PATH)
085                            .required(true)
086                            .build());
087                    start = m.end();
088                }
089                return parameters.toArray(new Parameter[parameters.size()]);
090            }
091        }
092        return null;
093    }
094
095    /**
096     * Merges {@link Parameter} values into the given {@code parameterList}, where conflicting
097     * {@link Parameter#getName() parameter-names} will be replaced, and new parameters will otherwise be added.
098     *
099     * @param parameterList Current list of parameters
100     * @param parameters Additional parameters to merge or {@code null}
101     * @return {@code parameterList} field
102     */
103    public static List<Parameter> mergeParameters(final List<Parameter> parameterList,
104            final Parameter... parameters) {
105        if (parameters != null) {
106            for (final Parameter parameter : parameters) {
107                // replace parameter if name already exists, otherwise add parameter to end of list
108                int replaceIndex = -1;
109                for (int i = 0; i < parameterList.size(); ++i) {
110                    if (parameterList.get(i).getName().equals(parameter.getName())) {
111                        replaceIndex = i;
112                        break;
113                    }
114                }
115                if (replaceIndex != -1) {
116                    parameterList.set(replaceIndex, parameter);
117                } else {
118                    parameterList.add(parameter);
119                }
120            }
121        }
122        return parameterList;
123    }
124}