1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.admin.service;
18
19 import static com.linecorp.centraldogma.server.storage.repository.FindOptions.FIND_ALL_WITH_CONTENT;
20 import static java.util.stream.Collectors.toList;
21
22 import java.io.IOException;
23 import java.util.List;
24 import java.util.Map.Entry;
25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.CompletionStage;
27
28 import com.fasterxml.jackson.databind.JsonNode;
29 import com.google.common.base.Splitter;
30 import com.google.common.base.Strings;
31 import com.google.common.collect.ImmutableList;
32 import com.google.common.collect.Maps;
33
34 import com.linecorp.armeria.common.AggregatedHttpRequest;
35 import com.linecorp.armeria.common.HttpResponse;
36 import com.linecorp.armeria.common.HttpStatus;
37 import com.linecorp.armeria.server.ServiceRequestContext;
38 import com.linecorp.armeria.server.annotation.Default;
39 import com.linecorp.armeria.server.annotation.ExceptionHandler;
40 import com.linecorp.armeria.server.annotation.Get;
41 import com.linecorp.armeria.server.annotation.Param;
42 import com.linecorp.armeria.server.annotation.Path;
43 import com.linecorp.armeria.server.annotation.Post;
44 import com.linecorp.armeria.server.annotation.Put;
45 import com.linecorp.centraldogma.common.Author;
46 import com.linecorp.centraldogma.common.Change;
47 import com.linecorp.centraldogma.common.Markup;
48 import com.linecorp.centraldogma.common.Query;
49 import com.linecorp.centraldogma.common.QueryType;
50 import com.linecorp.centraldogma.common.Revision;
51 import com.linecorp.centraldogma.internal.Jackson;
52 import com.linecorp.centraldogma.server.command.Command;
53 import com.linecorp.centraldogma.server.command.CommandExecutor;
54 import com.linecorp.centraldogma.server.internal.admin.auth.AuthUtil;
55 import com.linecorp.centraldogma.server.internal.admin.dto.ChangeDto;
56 import com.linecorp.centraldogma.server.internal.admin.dto.CommitDto;
57 import com.linecorp.centraldogma.server.internal.admin.dto.CommitMessageDto;
58 import com.linecorp.centraldogma.server.internal.admin.dto.EntryDto;
59 import com.linecorp.centraldogma.server.internal.admin.dto.RevisionDto;
60 import com.linecorp.centraldogma.server.internal.api.AbstractService;
61 import com.linecorp.centraldogma.server.internal.api.HttpApiExceptionHandler;
62 import com.linecorp.centraldogma.server.internal.api.auth.RequiresReadPermission;
63 import com.linecorp.centraldogma.server.internal.api.auth.RequiresWritePermission;
64 import com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager;
65 import com.linecorp.centraldogma.server.storage.repository.Repository;
66
67
68
69
70 @RequiresReadPermission
71 @ExceptionHandler(HttpApiExceptionHandler.class)
72 public class RepositoryService extends AbstractService {
73
74 private static final Object VOID = new Object();
75
76 private static final Splitter termSplitter = Splitter.on(',').trimResults().omitEmptyStrings();
77
78 private final ProjectApiManager projectApiManager;
79
80 public RepositoryService(ProjectApiManager projectApiManager, CommandExecutor executor) {
81 super(executor);
82 this.projectApiManager = projectApiManager;
83 }
84
85
86
87
88
89 @Get("/projects/{projectName}/repositories/{repoName}/revision/{revision}")
90 public RevisionDto normalizeRevision(@Param String projectName,
91 @Param String repoName,
92 @Param String revision) {
93 return DtoConverter.convert(projectApiManager.getProject(projectName).repos().get(repoName)
94 .normalizeNow(new Revision(revision)));
95 }
96
97
98
99
100
101 @Get("regex:/projects/(?<projectName>[^/]+)/repositories/(?<repoName>[^/]+)" +
102 "/files/revisions/(?<revision>[^/]+)(?<path>/.*$)")
103 public CompletionStage<EntryDto> getFile(@Param String projectName,
104 @Param String repoName,
105 @Param String revision,
106 @Param String path,
107 @Param @Default("IDENTITY") QueryType queryType,
108 @Param @Default("") String expression) {
109
110 final Query<?> query = Query.of(queryType,path, expression);
111 final Repository repo = projectApiManager.getProject(projectName).repos().get(repoName);
112 return repo.get(repo.normalizeNow(new Revision(revision)), query)
113 .thenApply(DtoConverter::convert);
114 }
115
116
117
118
119
120 @Post
121 @Put
122 @Path("/projects/{projectName}/repositories/{repoName}/files/revisions/{revision}")
123 @RequiresWritePermission
124 public CompletionStage<Object> addOrEditFile(@Param String projectName,
125 @Param String repoName,
126 @Param String revision,
127 AggregatedHttpRequest request,
128 ServiceRequestContext ctx) {
129 final Entry<CommitMessageDto, Change<?>> p = commitMessageAndChange(request);
130 final CommitMessageDto commitMessage = p.getKey();
131 final Change<?> change = p.getValue();
132 return push(projectName, repoName, new Revision(revision), AuthUtil.currentAuthor(ctx),
133 commitMessage.getSummary(), commitMessage.getDetail().getContent(),
134 Markup.valueOf(commitMessage.getDetail().getMarkup()), change)
135
136
137 .thenApply(unused -> VOID);
138 }
139
140
141
142
143
144 @Post("regex:/projects/(?<projectName>[^/]+)/repositories/(?<repoName>[^/]+)" +
145 "/delete/revisions/(?<revision>[^/]+)(?<path>/.*$)")
146 @RequiresWritePermission
147 public HttpResponse deleteFile(@Param String projectName,
148 @Param String repoName,
149 @Param String revision,
150 @Param String path,
151 AggregatedHttpRequest request,
152 ServiceRequestContext ctx) {
153 final CommitMessageDto commitMessage;
154 try {
155 final JsonNode node = Jackson.readTree(request.contentUtf8());
156 commitMessage = Jackson.convertValue(node.get("commitMessage"), CommitMessageDto.class);
157 } catch (IOException e) {
158 throw new IllegalArgumentException("invalid data to be parsed", e);
159 }
160
161 final CompletableFuture<?> future =
162 push(projectName, repoName, new Revision(revision), AuthUtil.currentAuthor(ctx),
163 commitMessage.getSummary(), commitMessage.getDetail().getContent(),
164 Markup.valueOf(commitMessage.getDetail().getMarkup()), Change.ofRemoval(path));
165
166 return HttpResponse.from(future.thenApply(unused -> HttpResponse.of(HttpStatus.OK)));
167 }
168
169
170
171
172
173 @Get("regex:/projects/(?<projectName>[^/]+)/repositories/(?<repoName>[^/]+)" +
174 "/history(?<path>/.*$)")
175 public CompletionStage<List<CommitDto>> getHistory(@Param String projectName,
176 @Param String repoName,
177 @Param String path,
178 @Param @Default("-1") String from,
179 @Param @Default("1") String to) {
180 return projectApiManager.getProject(projectName).repos().get(repoName)
181 .history(new Revision(from),
182 new Revision(to),
183 path + "**")
184 .thenApply(commits -> commits.stream()
185 .map(DtoConverter::convert)
186 .collect(toList()));
187 }
188
189
190
191
192
193 @Get("/projects/{projectName}/repositories/{repoName}/search/revisions/{revision}")
194 public CompletionStage<List<EntryDto>> search(@Param String projectName,
195 @Param String repoName,
196 @Param String revision,
197 @Param String term) {
198 return projectApiManager.getProject(projectName).repos().get(repoName)
199 .find(new Revision(revision), normalizeSearchTerm(term), FIND_ALL_WITH_CONTENT)
200 .thenApply(entries -> entries.values().stream()
201 .map(DtoConverter::convert)
202 .collect(toList()));
203 }
204
205
206
207
208
209 @Get("regex:/projects/(?<projectName>[^/]+)/repositories/(?<repoName>[^/]+)" +
210 "/diff(?<path>/.*$)")
211 public CompletionStage<List<ChangeDto>> getDiff(@Param String projectName,
212 @Param String repoName,
213 @Param String path,
214 @Param String from,
215 @Param String to) {
216 return projectApiManager.getProject(projectName).repos().get(repoName)
217 .diff(new Revision(from), new Revision(to), path)
218 .thenApply(changeMap -> changeMap.values().stream()
219 .map(DtoConverter::convert)
220 .collect(toList()));
221 }
222
223 private CompletableFuture<?> push(String projectName, String repoName,
224 Revision revision, Author author,
225 String commitSummary, String commitDetail, Markup commitMarkup,
226 Change<?> change) {
227 final Repository repo = projectApiManager.getProject(projectName).repos().get(repoName);
228 return push0(projectName, repoName, repo.normalizeNow(revision), author,
229 commitSummary, commitDetail, commitMarkup, change);
230 }
231
232 private CompletableFuture<?> push0(String projectName, String repoName,
233 Revision normalizedRev, Author author,
234 String commitSummary, String commitDetail, Markup commitMarkup,
235 Change<?> change) {
236 return execute(Command.push(author, projectName, repoName, normalizedRev,
237 commitSummary, commitDetail, commitMarkup, ImmutableList.of(change)));
238 }
239
240 private static Entry<CommitMessageDto, Change<?>> commitMessageAndChange(AggregatedHttpRequest request) {
241 try {
242 final JsonNode node = Jackson.readTree(request.contentUtf8());
243 final CommitMessageDto commitMessage =
244 Jackson.convertValue(node.get("commitMessage"), CommitMessageDto.class);
245 final EntryDto file = Jackson.convertValue(node.get("file"), EntryDto.class);
246 final Change<?> change;
247 switch (file.getType()) {
248 case "JSON":
249 change = Change.ofJsonUpsert(file.getPath(), file.getContent());
250 break;
251 case "TEXT":
252 change = Change.ofTextUpsert(file.getPath(), file.getContent());
253 break;
254 default:
255 throw new IllegalArgumentException("unsupported file type: " + file.getType());
256 }
257
258 return Maps.immutableEntry(commitMessage, change);
259 } catch (IOException e) {
260 throw new IllegalArgumentException("invalid data to be parsed", e);
261 }
262 }
263
264 private static String normalizeSearchTerm(final String term) {
265 if (Strings.isNullOrEmpty(term)) {
266 throw new IllegalArgumentException("term should not be empty");
267 }
268
269 final StringBuilder sb = new StringBuilder();
270
271 for (final String term0 : termSplitter.split(term)) {
272 if (sb.length() > 0) {
273 sb.append(',');
274 }
275
276 if (term0.matches(".*[/*]+.*")) {
277 sb.append(term0);
278 } else {
279 sb.append('*').append(term0).append('*');
280 }
281 }
282 return sb.toString();
283 }
284 }