1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.api.converter;
18
19 import static com.google.common.base.Ascii.toLowerCase;
20 import static com.google.common.base.Strings.isNullOrEmpty;
21
22 import java.lang.reflect.ParameterizedType;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.concurrent.TimeUnit;
27
28 import javax.annotation.Nullable;
29
30 import com.google.common.annotations.VisibleForTesting;
31 import com.google.common.base.MoreObjects;
32 import com.google.common.base.Splitter;
33
34 import com.linecorp.armeria.common.AggregatedHttpRequest;
35 import com.linecorp.armeria.common.HttpHeaderNames;
36 import com.linecorp.armeria.server.ServiceRequestContext;
37 import com.linecorp.armeria.server.annotation.RequestConverterFunction;
38 import com.linecorp.centraldogma.common.Revision;
39
40
41
42
43
44 public final class WatchRequestConverter implements RequestConverterFunction {
45
46 private static final long DEFAULT_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(120);
47
48 private static final Splitter preferenceSplitter = Splitter.on(',').omitEmptyStrings().trimResults();
49
50 private static final Splitter tokenSplitter = Splitter.on('=').omitEmptyStrings().trimResults();
51
52 private static final String NOTIFY_ENTRY_NOT_FOUND = "notify-entry-not-found";
53
54
55
56
57
58 @Override
59 @Nullable
60 public WatchRequest convertRequest(
61 ServiceRequestContext ctx, AggregatedHttpRequest request, Class<?> expectedResultType,
62 @Nullable ParameterizedType expectedParameterizedResultType) throws Exception {
63
64 final String ifNoneMatch = request.headers().get(HttpHeaderNames.IF_NONE_MATCH);
65 if (isNullOrEmpty(ifNoneMatch)) {
66 return null;
67 }
68
69 final Revision lastKnownRevision = new Revision(extractRevision(ifNoneMatch));
70 final String prefer = request.headers().get(HttpHeaderNames.PREFER);
71 final long timeoutMillis;
72 final boolean notifyEntryNotFound;
73 if (!isNullOrEmpty(prefer)) {
74 final Map<String, String> tokens = extract(prefer);
75 timeoutMillis = timeoutMillis(tokens, prefer);
76 notifyEntryNotFound = notifyEntryNotFound(tokens);
77 } else {
78 timeoutMillis = DEFAULT_TIMEOUT_MILLIS;
79 notifyEntryNotFound = false;
80 }
81
82 return new WatchRequest(lastKnownRevision, timeoutMillis, notifyEntryNotFound);
83 }
84
85 @VisibleForTesting
86 String extractRevision(String ifNoneMatch) {
87 final int length = ifNoneMatch.length();
88
89
90
91
92
93 if (length > 2 && ifNoneMatch.charAt(0) == '"' &&
94 ifNoneMatch.charAt(length - 1) == '"') {
95 return ifNoneMatch.substring(1, length - 1);
96 }
97
98 if (length > 4 && ifNoneMatch.startsWith("W/\"") &&
99 ifNoneMatch.charAt(length - 1) == '"') {
100 return ifNoneMatch.substring(3, length - 1);
101 }
102
103 return ifNoneMatch;
104 }
105
106
107 private static Map<String, String> extract(String preferHeader) {
108 final Iterable<String> preferences = preferenceSplitter.split(preferHeader);
109 final HashMap<String, String> tokens = new HashMap<>();
110 for (String preference : preferences) {
111 final Iterable<String> split = tokenSplitter.split(preference);
112 final Iterator<String> iterator = split.iterator();
113 if (iterator.hasNext()) {
114 final String token = iterator.next();
115 if (iterator.hasNext()) {
116 final String value = iterator.next();
117 tokens.put(toLowerCase(token), value);
118 }
119 }
120 }
121 return tokens;
122 }
123
124 private static long timeoutMillis(Map<String, String> tokens, String preferHeader) {
125 final String wait = tokens.get("wait");
126 if (wait == null) {
127 return rejectPreferHeader(preferHeader, "wait=seconds");
128 }
129
130 final long timeoutSeconds;
131 try {
132 timeoutSeconds = Long.parseLong(wait);
133 } catch (NumberFormatException e) {
134 return rejectPreferHeader(preferHeader, "wait=seconds");
135 }
136
137 if (timeoutSeconds <= 0) {
138 return rejectPreferHeader(preferHeader, "seconds > 0");
139 }
140 return TimeUnit.SECONDS.toMillis(timeoutSeconds);
141 }
142
143 private static long rejectPreferHeader(String preferHeader, String expected) {
144 throw new IllegalArgumentException("invalid prefer header: " + preferHeader +
145 " (expected: " + expected + ')');
146 }
147
148 private static boolean notifyEntryNotFound(Map<String, String> tokens) {
149 final String notifyEntryNotFound = tokens.get(NOTIFY_ENTRY_NOT_FOUND);
150 if ("true".equalsIgnoreCase(notifyEntryNotFound)) {
151 return true;
152 }
153
154 return false;
155 }
156
157 public static class WatchRequest {
158 private final Revision lastKnownRevision;
159 private final long timeoutMillis;
160 private final boolean notifyEntryNotFound;
161
162 WatchRequest(Revision lastKnownRevision, long timeoutMillis, boolean notifyEntryNotFound) {
163 this.lastKnownRevision = lastKnownRevision;
164 this.timeoutMillis = timeoutMillis;
165 this.notifyEntryNotFound = notifyEntryNotFound;
166 }
167
168 public Revision lastKnownRevision() {
169 return lastKnownRevision;
170 }
171
172 public long timeoutMillis() {
173 return timeoutMillis;
174 }
175
176 public boolean notifyEntryNotFound() {
177 return notifyEntryNotFound;
178 }
179
180 @Override
181 public String toString() {
182 return MoreObjects.toStringHelper(this)
183 .add("lastKnownRevision", lastKnownRevision)
184 .add("timeoutMillis", timeoutMillis)
185 .toString();
186 }
187 }
188 }