summaryrefslogtreecommitdiffstats
path: root/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/RestUri.java
blob: 975075fd2fac9b5df04f315164bc15767a2679d1 (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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.document.restapi;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import static com.yahoo.jdisc.Response.Status.*;

/**
 * Represents the request URI with its values.
 *
 * @author dybis
 */
public class RestUri {

    public static final char NUMBER_STREAMING = 'n';
    public static final char GROUP_STREAMING = 'g';
    public static final String DOCUMENT = "document";
    public static final String V_1 = "v1";
    public static final String ID = "id:";

    public enum apiErrorCodes {
        ERROR_ID_BASIC_USAGE(-1),
        ERROR_ID_DECODING_PATH(-2),
        VISITOR_ERROR(-3),
        NO_ROUTE_WHEN_NOT_PART_OF_MESSAGEBUS(-4),
        SEVERAL_CLUSTERS(-5),
        URL_PARSING(-6),
        INVALID_CREATE_VALUE(-7),
        TOO_MANY_PARALLEL_REQUESTS(-8),
        MISSING_CLUSTER(-9), INTERNAL_EXCEPTION(-9),
        DOCUMENT_CONDITION_NOT_MET(-10),
        DOCUMENT_EXCEPTION(-11),
        PARSER_ERROR(-11),
        GROUP_AND_EXPRESSION_ERROR(-12),
        TIME_OUT(-13),
        INTERRUPTED(-14),
        UNSPECIFIED(-15),
        UNKNOWN_BUCKET_SPACE(-16);

        public final long value;
        apiErrorCodes(long value) {
            this.value = value;
        }
    }

    /**
     * Represents the "grouping" part of document id which can be used with streaming model.
     */
    public static class Group {
        public final char name;
        public final String value;
        Group(char name, String value) {
            this.name = name;
            this.value = value;
        }
    }
    private final String namespace;
    private final String documentType;
    private final String docId;
    private Optional<Group> group = Optional.empty();
    private final String rawPath;

    public boolean isRootOnly() {
        return namespace == null;
    }

    public String getRawPath() {
        return rawPath;
    }

    public String getNamespace() {
        return namespace;
    }

    public String getDocumentType() {
        return documentType;
    }

    public String getDocId() {
        return docId;
    }

    public Optional<Group> getGroup() {
        return group;
    }

    public String generateFullId() {
        return ID + namespace + ":" + documentType + ":"
            + group.map(g -> String.format("%s=%s", g.name, g.value)).orElse("")
            + ":" + docId;
    }

    static class PathParser {
        public static final long ERROR_ID_DECODING_PATH = -10L;
        final List<String> rawParts;
        final String originalPath;
        int readPos = 0;
        public PathParser(String path) {
            this.originalPath = path;
            this.rawParts = Splitter.on('/').splitToList(path);
        }

        boolean hasNextToken() {
            return readPos < rawParts.size();
        }

        String nextTokenOrException() throws RestApiException {
            if (readPos >= rawParts.size()) {
                throwUsage(originalPath);
            }
            String nextToken = rawParts.get(readPos++);
            return urlDecodeOrException(nextToken);
        }

        String restOfPath() throws RestApiException {
            String rawId = Joiner.on("/").join(rawParts.listIterator(readPos));
            return urlDecodeOrException(rawId);
        }

        String urlDecodeOrException(String url) throws RestApiException {
            try {
                return URLDecoder.decode(url, StandardCharsets.UTF_8.name());
            } catch (UnsupportedEncodingException e) {
                throw new RestApiException(Response.createErrorResponse(BAD_REQUEST,"Problems decoding the URI: " + e.getMessage(), apiErrorCodes.ERROR_ID_DECODING_PATH));
            }
        }
    }

    public RestUri(URI uri) throws RestApiException {
        rawPath = uri.getRawPath();
        PathParser pathParser = new PathParser(rawPath);
        if (! pathParser.nextTokenOrException().equals("") ||
                ! pathParser.nextTokenOrException().equals(DOCUMENT) ||
                ! pathParser.nextTokenOrException().equals(V_1)) {
            throwUsage(uri.getRawPath());
        }
        // If /document/v1 root request, there's an empty token at the end.
        String maybeNamespace = pathParser.nextTokenOrException();
        if (maybeNamespace.isEmpty()) {
            namespace = null;
            documentType = null;
            docId = null;
            return;
        }
        namespace = maybeNamespace;
        documentType = pathParser.nextTokenOrException();
        switch (pathParser.nextTokenOrException()) {
            case "number":
                group = Optional.of(new Group(NUMBER_STREAMING, pathParser.nextTokenOrException()));
                break;
            case "docid":
                group = Optional.empty();
                break;
            case "group":
                group = Optional.of(new Group(GROUP_STREAMING, pathParser.nextTokenOrException()));
                break;
            default: throwUsage(uri.getRawPath());
        }
        docId = pathParser.restOfPath();
    }

    private static void throwUsage(String inputPath) throws RestApiException {
        throw new RestApiException(Response.createErrorResponse(BAD_REQUEST,
                "Expected: " +
                        ".../{namespace}/{document-type}/group/{name}/[{user-specified}]   " +
                        ".../{namespace}/{document-type}/docid/[{user-specified}]  : but got " + inputPath, apiErrorCodes.ERROR_ID_BASIC_USAGE));
    }

}