1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.metadata;
18
19 import static com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache.logger;
20 import static java.util.Objects.requireNonNull;
21
22 import java.util.Objects;
23 import java.util.concurrent.CompletableFuture;
24
25 import com.fasterxml.jackson.databind.JsonNode;
26 import com.google.common.base.MoreObjects.ToStringHelper;
27 import com.google.common.collect.ImmutableList;
28
29 import com.linecorp.armeria.common.util.Exceptions;
30 import com.linecorp.centraldogma.common.Author;
31 import com.linecorp.centraldogma.common.Change;
32 import com.linecorp.centraldogma.common.Entry;
33 import com.linecorp.centraldogma.common.Markup;
34 import com.linecorp.centraldogma.common.RedundantChangeException;
35 import com.linecorp.centraldogma.common.Revision;
36 import com.linecorp.centraldogma.internal.Jackson;
37 import com.linecorp.centraldogma.server.command.Command;
38 import com.linecorp.centraldogma.server.command.CommandExecutor;
39 import com.linecorp.centraldogma.server.command.CommitResult;
40 import com.linecorp.centraldogma.server.command.ContentTransformer;
41 import com.linecorp.centraldogma.server.storage.project.ProjectManager;
42 import com.linecorp.centraldogma.server.storage.repository.AbstractCacheableCall;
43 import com.linecorp.centraldogma.server.storage.repository.HasWeight;
44 import com.linecorp.centraldogma.server.storage.repository.Repository;
45
46 final class RepositorySupport<T> {
47
48 private final ProjectManager projectManager;
49 private final CommandExecutor executor;
50 private final Class<T> entryClass;
51
52 RepositorySupport(ProjectManager projectManager, CommandExecutor executor,
53 Class<T> entryClass) {
54 this.projectManager = requireNonNull(projectManager, "projectManager");
55 this.executor = requireNonNull(executor, "executor");
56 this.entryClass = requireNonNull(entryClass, "entryClass");
57 }
58
59 public ProjectManager projectManager() {
60 return projectManager;
61 }
62
63 CompletableFuture<HolderWithRevision<T>> fetch(String projectName, String repoName, String path) {
64 requireNonNull(projectName, "projectName");
65 requireNonNull(repoName, "repoName");
66 return fetch(projectManager().get(projectName).repos().get(repoName), path);
67 }
68
69 private CompletableFuture<HolderWithRevision<T>> fetch(Repository repository, String path) {
70 requireNonNull(path, "path");
71 final Revision revision = normalize(repository);
72 return fetch(repository, path, revision);
73 }
74
75 private CompletableFuture<HolderWithRevision<T>> fetch(Repository repository, String path,
76 Revision revision) {
77 requireNonNull(repository, "repository");
78 requireNonNull(path, "path");
79 requireNonNull(revision, "revision");
80 final CacheableFetchCall<T> cacheableFetchCall = new CacheableFetchCall<>(repository, revision, path,
81 entryClass);
82 return repository.execute(cacheableFetchCall);
83 }
84
85 CompletableFuture<Revision> push(String projectName, String repoName,
86 Author author, String commitSummary, Change<?> change) {
87 return push(projectName, repoName, author, commitSummary, change, Revision.HEAD);
88 }
89
90 private CompletableFuture<Revision> push(String projectName, String repoName, Author author,
91 String commitSummary, Change<?> change, Revision revision) {
92 requireNonNull(change, "change");
93 return push(projectName, repoName, author, commitSummary, ImmutableList.of(change), revision);
94 }
95
96 private CompletableFuture<Revision> push(String projectName, String repoName, Author author,
97 String commitSummary, Iterable<Change<?>> changes,
98 Revision revision) {
99 requireNonNull(projectName, "projectName");
100 requireNonNull(repoName, "repoName");
101 requireNonNull(author, "author");
102 requireNonNull(commitSummary, "commitSummary");
103 requireNonNull(changes, "changes");
104
105 return executor.execute(Command.push(author, projectName, repoName, revision, commitSummary, "",
106 Markup.PLAINTEXT, changes))
107 .thenApply(CommitResult::revision);
108 }
109
110 CompletableFuture<Revision> push(String projectName, String repoName,
111 Author author, String commitSummary,
112 ContentTransformer<JsonNode> transformer) {
113 return push(projectName, repoName, author, commitSummary, transformer, false);
114 }
115
116 CompletableFuture<Revision> push(String projectName, String repoName,
117 Author author, String commitSummary,
118 ContentTransformer<JsonNode> transformer, boolean forcePush) {
119 requireNonNull(projectName, "projectName");
120 requireNonNull(repoName, "repoName");
121 requireNonNull(author, "author");
122 requireNonNull(commitSummary, "commitSummary");
123 requireNonNull(transformer, "transformer");
124
125 Command<CommitResult> command = Command.transform(null, author, projectName, repoName,
126 Revision.HEAD,
127 commitSummary, "", Markup.PLAINTEXT,
128 transformer);
129 if (forcePush) {
130 command = Command.forcePush(command);
131 }
132 return executor.execute(command)
133 .thenApply(CommitResult::revision)
134 .exceptionally(cause -> {
135 final Throwable peeled = Exceptions.peel(cause);
136 if (peeled instanceof RedundantChangeException) {
137 final Revision revision = ((RedundantChangeException) peeled).headRevision();
138 assert revision != null;
139 return revision;
140 }
141 return Exceptions.throwUnsafely(peeled);
142 });
143 }
144
145 static Revision normalize(Repository repository) {
146 requireNonNull(repository, "repository");
147 try {
148 return repository.normalizeNow(Revision.HEAD);
149 } catch (Throwable cause) {
150 return Exceptions.throwUnsafely(cause);
151 }
152 }
153
154
155 private static class CacheableFetchCall<U> extends AbstractCacheableCall<HolderWithRevision<U>> {
156
157 private final Revision revision;
158 private final String path;
159 private final Class<U> entryClass;
160 private final int hashCode;
161
162 CacheableFetchCall(Repository repo, Revision revision, String path, Class<U> entryClass) {
163 super(repo);
164 this.revision = revision;
165 this.path = path;
166 this.entryClass = entryClass;
167
168 hashCode = Objects.hash(revision, path, entryClass) * 31 + System.identityHashCode(repo);
169 assert !revision.isRelative();
170 }
171
172 @Override
173 public int weigh(HolderWithRevision<U> value) {
174 int weight = path.length();
175 final U object = value.object();
176 if (object instanceof HasWeight) {
177 weight += ((HasWeight) object).weight();
178 }
179 return weight;
180 }
181
182 @Override
183 public CompletableFuture<HolderWithRevision<U>> execute() {
184 logger.debug("Cache miss: {}", this);
185 return repo().get(revision, path)
186 .thenApply(this::convertWithJackson)
187 .thenApply((U obj) -> HolderWithRevision.of(obj, revision));
188 }
189
190 @SuppressWarnings("unchecked")
191 U convertWithJackson(Entry<?> entry) {
192 requireNonNull(entry, "entry");
193 try {
194 return Jackson.treeToValue(((Entry<JsonNode>) entry).content(), entryClass);
195 } catch (Throwable cause) {
196 return Exceptions.throwUnsafely(cause);
197 }
198 }
199
200 @Override
201 public int hashCode() {
202 return hashCode;
203 }
204
205 @Override
206 public boolean equals(Object o) {
207 if (!super.equals(o)) {
208 return false;
209 }
210
211 final CacheableFetchCall<?> that = (CacheableFetchCall<?>) o;
212 return revision.equals(that.revision) &&
213 path.equals(that.path) &&
214 entryClass == that.entryClass;
215 }
216
217 @Override
218 protected void toString(ToStringHelper helper) {
219 helper.add("revision", revision)
220 .add("path", path)
221 .add("entryClass", entryClass);
222 }
223 }
224 }