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