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