1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.common;
18
19 import static com.linecorp.centraldogma.internal.Util.validateDirPath;
20 import static com.linecorp.centraldogma.internal.Util.validateFilePath;
21 import static java.util.Objects.requireNonNull;
22
23 import java.io.File;
24 import java.io.IOError;
25 import java.io.IOException;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.Files;
28 import java.nio.file.Path;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
33
34 import javax.annotation.Nullable;
35
36 import com.fasterxml.jackson.annotation.JsonProperty;
37 import com.fasterxml.jackson.databind.JsonNode;
38 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
39
40 import com.linecorp.centraldogma.internal.Jackson;
41 import com.linecorp.centraldogma.internal.Util;
42 import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
43 import com.linecorp.centraldogma.internal.jsonpatch.ReplaceMode;
44
45 import difflib.DiffUtils;
46 import difflib.Patch;
47
48
49
50
51 @JsonDeserialize(as = DefaultChange.class)
52 public interface Change<T> {
53
54
55
56
57
58
59
60
61
62
63
64
65 static Change<String> ofTextUpsert(String path, String text) {
66 requireNonNull(text, "text");
67 validateFilePath(path, "path");
68 if (EntryType.guessFromPath(path) == EntryType.JSON) {
69 throw new ChangeFormatException("invalid file type: " + path +
70 " (expected: a non-JSON file). Use Change.ofJsonUpsert() instead");
71 }
72 return new DefaultChange<>(path, ChangeType.UPSERT_TEXT, text);
73 }
74
75
76
77
78
79
80
81
82
83 static Change<JsonNode> ofJsonUpsert(String path, String jsonText) {
84 requireNonNull(jsonText, "jsonText");
85
86 final JsonNode jsonNode;
87 try {
88 jsonNode = Jackson.readTree(jsonText);
89 } catch (IOException e) {
90 throw new ChangeFormatException("failed to read a value as a JSON tree", e);
91 }
92
93 return new DefaultChange<>(path, ChangeType.UPSERT_JSON, jsonNode);
94 }
95
96
97
98
99
100
101
102 static Change<JsonNode> ofJsonUpsert(String path, JsonNode jsonNode) {
103 requireNonNull(jsonNode, "jsonNode");
104 return new DefaultChange<>(path, ChangeType.UPSERT_JSON, jsonNode);
105 }
106
107
108
109
110
111
112 static Change<Void> ofRemoval(String path) {
113 return new DefaultChange<>(path, ChangeType.REMOVE, null);
114 }
115
116
117
118
119
120
121
122 static Change<String> ofRename(String oldPath, String newPath) {
123 validateFilePath(oldPath, "oldPath");
124 validateFilePath(newPath, "newPath");
125 return new DefaultChange<>(oldPath, ChangeType.RENAME, newPath);
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139
140 static Change<String> ofTextPatch(String path, @Nullable String oldText, String newText) {
141 validateFilePath(path, "path");
142 requireNonNull(newText, "newText");
143 if (EntryType.guessFromPath(path) == EntryType.JSON) {
144 throw new ChangeFormatException("invalid file type: " + path +
145 " (expected: a non-JSON file). Use Change.ofJsonPatch() instead");
146 }
147
148 final List<String> oldLineList = oldText == null ? Collections.emptyList()
149 : Util.stringToLines(oldText);
150 final List<String> newLineList = Util.stringToLines(newText);
151
152 final Patch<String> patch = DiffUtils.diff(oldLineList, newLineList);
153 final List<String> unifiedDiff = DiffUtils.generateUnifiedDiff(path, path, oldLineList, patch, 3);
154
155 return new DefaultChange<>(path, ChangeType.APPLY_TEXT_PATCH, String.join("\n", unifiedDiff));
156 }
157
158
159
160
161
162
163
164
165
166
167
168
169
170 static Change<String> ofTextPatch(String path, String textPatch) {
171 validateFilePath(path, "path");
172 requireNonNull(textPatch, "textPatch");
173 if (EntryType.guessFromPath(path) == EntryType.JSON) {
174 throw new ChangeFormatException("invalid file type: " + path +
175 " (expected: a non-JSON file). Use Change.ofJsonPatch() instead");
176 }
177
178 return new DefaultChange<>(path, ChangeType.APPLY_TEXT_PATCH, textPatch);
179 }
180
181
182
183
184
185
186
187
188
189
190
191 static Change<JsonNode> ofJsonPatch(String path, @Nullable String oldJsonText, String newJsonText) {
192 requireNonNull(newJsonText, "newJsonText");
193
194 final JsonNode oldJsonNode;
195 final JsonNode newJsonNode;
196 try {
197 oldJsonNode = oldJsonText == null ? Jackson.nullNode
198 : Jackson.readTree(oldJsonText);
199 newJsonNode = Jackson.readTree(newJsonText);
200 } catch (IOException e) {
201 throw new ChangeFormatException("failed to read a value as a JSON tree", e);
202 }
203
204 return new DefaultChange<>(path, ChangeType.APPLY_JSON_PATCH,
205 JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE).toJson());
206 }
207
208
209
210
211
212
213
214
215 static Change<JsonNode> ofJsonPatch(String path, @Nullable JsonNode oldJsonNode, JsonNode newJsonNode) {
216 requireNonNull(newJsonNode, "newJsonNode");
217
218 if (oldJsonNode == null) {
219 oldJsonNode = Jackson.nullNode;
220 }
221
222 return new DefaultChange<>(path, ChangeType.APPLY_JSON_PATCH,
223 JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE).toJson());
224 }
225
226
227
228
229
230
231
232
233
234 static Change<JsonNode> ofJsonPatch(String path, String jsonPatchText) {
235 requireNonNull(jsonPatchText, "jsonPatchText");
236
237 final JsonNode jsonPatchNode;
238 try {
239 jsonPatchNode = Jackson.readTree(jsonPatchText);
240 } catch (IOException e) {
241 throw new ChangeFormatException("failed to read a value as a JSON tree", e);
242 }
243
244 return ofJsonPatch(path, jsonPatchNode);
245 }
246
247
248
249
250
251
252
253 static Change<JsonNode> ofJsonPatch(String path, JsonNode jsonPatchNode) {
254 requireNonNull(jsonPatchNode, "jsonPatchNode");
255
256 return new DefaultChange<>(path, ChangeType.APPLY_JSON_PATCH, jsonPatchNode);
257 }
258
259
260
261
262
263
264
265
266
267
268 static List<Change<?>> fromDirectory(Path sourcePath, String targetPath) {
269 requireNonNull(sourcePath, "sourcePath");
270 validateDirPath(targetPath, "targetPath");
271
272 if (!Files.isDirectory(sourcePath)) {
273 throw new IllegalArgumentException("sourcePath: " + sourcePath + " (must be a directory)");
274 }
275
276 final String finalTargetPath;
277 if (!targetPath.endsWith("/")) {
278 finalTargetPath = targetPath + '/';
279 } else {
280 finalTargetPath = targetPath;
281 }
282
283 try (Stream<Path> s = Files.find(sourcePath, Integer.MAX_VALUE, (p, a) -> a.isRegularFile())) {
284 final int baseLength = sourcePath.toString().length() + 1;
285 return s.map(sourceFilePath -> {
286 final String targetFilePath =
287 finalTargetPath +
288 sourceFilePath.toString().substring(baseLength).replace(File.separatorChar, '/');
289
290 return fromFile(sourceFilePath, targetFilePath);
291 }).collect(Collectors.toList());
292 } catch (IOException e) {
293 throw new IOError(e);
294 }
295 }
296
297
298
299
300
301
302
303 static Change<?> fromFile(Path sourcePath, String targetPath) {
304 requireNonNull(sourcePath, "sourcePath");
305 validateFilePath(targetPath, "targetPath");
306
307 if (!Files.isRegularFile(sourcePath)) {
308 throw new IllegalArgumentException("sourcePath: " + sourcePath + " (must be a regular file)");
309 }
310
311 if (targetPath.endsWith("/")) {
312 throw new IllegalArgumentException("targetPath: " + targetPath + " (must be a regular file path)");
313 }
314
315 final EntryType entryType = EntryType.guessFromPath(targetPath);
316 final String content;
317 try {
318 content = new String(Files.readAllBytes(sourcePath), StandardCharsets.UTF_8);
319 } catch (IOException e) {
320 throw new IOError(e);
321 }
322
323 switch (entryType) {
324 case JSON:
325 return ofJsonUpsert(targetPath, content);
326 case TEXT:
327 return ofTextUpsert(targetPath, content);
328 default:
329 throw new Error("unexpected entry type: " + entryType);
330 }
331 }
332
333
334
335
336 @JsonProperty
337 ChangeType type();
338
339
340
341
342 @JsonProperty
343 String path();
344
345
346
347
348 @Nullable
349 @JsonProperty
350 T content();
351
352
353
354
355 @Nullable
356 String contentAsText();
357 }