summaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/restapi/Path.java
blob: fe65245fd156b3ea13094cb76b09e86ef980fa78 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.restapi;

import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

/**
 * A path which is able to match strings containing bracketed placeholders and return the
 * values given at the placeholders. The path is split on '/', and each part is then URL decoded.
 * 
 * E.g a path /a/1/bar/fuz/baz/%62%2f
 * will match /a/{foo}/bar/{b}/baz/{c}
 * and return foo=1, b=fuz, and c=c/
 * 
 * Only full path elements may be placeholders, i.e /a{bar} is not interpreted as one.
 * 
 * If the path spec ends with /{*}, it will match urls with any rest path.
 * The rest path (not including the trailing slash) will be available as getRest().
 * 
 * Note that for convenience in common use this has state which changes as a side effect of each matches
 * invocation. It is therefore for single thread use.
 *
 * An optional prefix can be used to match the path spec against an alternative path. This
 * is used when you have alternative paths mapped to the same resource.
 *
 * @author bratseth
 */
public class Path {

    // This path
    private final String pathString;
    private final String optionalPrefix;
    private final String[] elements;

    // Info about the last match
    private final Map<String, String> values = new HashMap<>();
    private String rest = "";

    /**
     * @deprecated use {@link #Path(URI)} for correct handling of URL encoded paths.
     */
    @Deprecated
    public Path(String path) {
        this(path, "");
    }

    /**
     * @deprecated use {@link #Path(URI, String)} for correct handling of URL encoded paths.
     */
    @Deprecated
    public Path(String path, String optionalPrefix) {
        this.optionalPrefix = optionalPrefix;
        this.pathString = path;
        this.elements = path.split("/");
    }

    public Path(URI uri) {
        this(uri, "");
    }

    public Path(URI uri, String optionalPrefix) {
        this.optionalPrefix = optionalPrefix;
        this.pathString = uri.getRawPath();
        this.elements = Stream.of(this.pathString.split("/"))
                              .map(part -> URLDecoder.decode(part, StandardCharsets.UTF_8))
                              .toArray(String[]::new);
    }

    private boolean matchesInner(String pathSpec) {
        values.clear();
        String[] specElements = pathSpec.split("/");
        boolean matchPrefix = false;
        if (specElements.length > 1 && specElements[specElements.length-1].equals("{*}")) {
            matchPrefix = true;
            specElements = Arrays.copyOf(specElements, specElements.length-1);
        }

        if (matchPrefix) {
            if (this.elements.length < specElements.length) return false;
        }
        else { // match exact
            if (this.elements.length != specElements.length) return false;
        }
        
        for (int i = 0; i < specElements.length; i++) {
            if (specElements[i].startsWith("{") && specElements[i].endsWith("}")) // placeholder
                values.put(specElements[i].substring(1, specElements[i].length()-1), elements[i]);
            else if ( ! specElements[i].equals(this.elements[i]))
                return false;
        }
        
        if (matchPrefix) {
            StringBuilder rest = new StringBuilder();
            for (int i = specElements.length; i < this.elements.length; i++)
                rest.append(elements[i]).append("/");
            if ( ! pathString.endsWith("/") && rest.length() > 0)
                rest.setLength(rest.length() - 1);
            this.rest = rest.toString();
        }
        
        return true;
    }

    /**
     * Parses the path according to pathSpec - must be called prior to {@link #get}
     *
     * Returns whether this path matches the given template string.
     * If the given template has placeholders, their values (accessible by get) are reset by calling this,
     * whether or not the path matches the given template.
     *
     * This will NOT match empty path elements.
     *
     * @param pathSpec the path string to match to this
     * @return true if the string matches, false otherwise
     */
    public boolean matches(String pathSpec) {
        if (matchesInner(pathSpec)) return true;
        if (optionalPrefix.isEmpty()) return false;
        return matchesInner(optionalPrefix + pathSpec);
    }

    /**
     * Returns the value of the given template variable in the last path matched, or null 
     * if the previous matches call returned false or if this has not matched anything yet.
     */
    public String get(String placeholder) {
        return values.get(placeholder);
    }

    /**
     * Returns the rest of the last matched path.
     * This is always the empty string (never null) unless the path spec ends with {*}
     */
    public String getRest() { return rest; }

    public String asString() {
        return pathString;
    }

    @Override
    public String toString() {
        return "path '" + String.join("/", elements) + "'";
    }
    
}