1   /*
2    * Copyright 2019 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
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     // TODO(minwoox): Consider generalizing this class.
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 }