1   /*
2    * Copyright 2017 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.internal.storage.project;
18  
19  import static com.linecorp.centraldogma.server.internal.storage.project.ProjectInitializer.INTERNAL_PROJECT_DOGMA;
20  import static com.linecorp.centraldogma.server.metadata.MetadataService.METADATA_JSON;
21  import static java.util.Objects.requireNonNull;
22  
23  import java.io.File;
24  import java.util.concurrent.Executor;
25  import java.util.concurrent.atomic.AtomicReference;
26  
27  import javax.annotation.Nullable;
28  
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import com.fasterxml.jackson.core.JsonParseException;
33  import com.fasterxml.jackson.databind.JsonMappingException;
34  import com.fasterxml.jackson.databind.JsonNode;
35  import com.google.common.collect.ImmutableMap;
36  
37  import com.linecorp.centraldogma.common.Author;
38  import com.linecorp.centraldogma.common.CentralDogmaException;
39  import com.linecorp.centraldogma.common.Change;
40  import com.linecorp.centraldogma.common.Entry;
41  import com.linecorp.centraldogma.common.Markup;
42  import com.linecorp.centraldogma.common.ProjectExistsException;
43  import com.linecorp.centraldogma.common.ProjectNotFoundException;
44  import com.linecorp.centraldogma.common.Query;
45  import com.linecorp.centraldogma.common.RepositoryExistsException;
46  import com.linecorp.centraldogma.common.Revision;
47  import com.linecorp.centraldogma.internal.Jackson;
48  import com.linecorp.centraldogma.internal.Util;
49  import com.linecorp.centraldogma.server.internal.storage.repository.DefaultMetaRepository;
50  import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache;
51  import com.linecorp.centraldogma.server.internal.storage.repository.cache.CachingRepositoryManager;
52  import com.linecorp.centraldogma.server.internal.storage.repository.git.GitRepositoryManager;
53  import com.linecorp.centraldogma.server.metadata.Member;
54  import com.linecorp.centraldogma.server.metadata.PerRolePermissions;
55  import com.linecorp.centraldogma.server.metadata.ProjectMetadata;
56  import com.linecorp.centraldogma.server.metadata.ProjectRole;
57  import com.linecorp.centraldogma.server.metadata.RepositoryMetadata;
58  import com.linecorp.centraldogma.server.metadata.UserAndTimestamp;
59  import com.linecorp.centraldogma.server.storage.project.Project;
60  import com.linecorp.centraldogma.server.storage.repository.MetaRepository;
61  import com.linecorp.centraldogma.server.storage.repository.Repository;
62  import com.linecorp.centraldogma.server.storage.repository.RepositoryManager;
63  
64  public class DefaultProject implements Project {
65  
66      private static final Logger logger = LoggerFactory.getLogger(DefaultProject.class);
67  
68      private final String name;
69      private final long creationTimeMillis;
70      private final Author author;
71      final RepositoryManager repos;
72      private final AtomicReference<MetaRepository> metaRepo = new AtomicReference<>();
73  
74      /**
75       * Opens an existing project.
76       */
77      DefaultProject(File rootDir, Executor repositoryWorker, Executor purgeWorker,
78                     @Nullable RepositoryCache cache) {
79          requireNonNull(rootDir, "rootDir");
80          requireNonNull(repositoryWorker, "repositoryWorker");
81  
82          if (!rootDir.exists()) {
83              throw new ProjectNotFoundException(rootDir.toString());
84          }
85  
86          name = rootDir.getName();
87          repos = newRepoManager(rootDir, repositoryWorker, purgeWorker, cache);
88  
89          boolean success = false;
90          try {
91              createReservedRepos(System.currentTimeMillis());
92              final UserAndTimestamp creation = metadataCreation();
93              if (creation != null) {
94                  creationTimeMillis = creation.timestampMillis();
95                  author = Author.ofEmail(creation.user());
96              } else {
97                  creationTimeMillis = repos.get(REPO_DOGMA).creationTimeMillis();
98                  author = repos.get(REPO_DOGMA).author();
99              }
100             success = true;
101         } finally {
102             if (!success) {
103                 repos.close(() -> new CentralDogmaException("failed to initialize internal repositories"));
104             }
105         }
106     }
107 
108     /**
109      * Creates a new project.
110      */
111     DefaultProject(File rootDir, Executor repositoryWorker, Executor purgeWorker,
112                    long creationTimeMillis, Author author, @Nullable RepositoryCache cache) {
113         requireNonNull(rootDir, "rootDir");
114         requireNonNull(repositoryWorker, "repositoryWorker");
115 
116         if (rootDir.exists()) {
117             throw new ProjectExistsException(rootDir.getName());
118         }
119 
120         name = rootDir.getName();
121         repos = newRepoManager(rootDir, repositoryWorker, purgeWorker, cache);
122 
123         boolean success = false;
124         try {
125             createReservedRepos(creationTimeMillis);
126             initializeMetadata(creationTimeMillis, author);
127             this.creationTimeMillis = creationTimeMillis;
128             this.author = author;
129             success = true;
130         } finally {
131             if (!success) {
132                 repos.close(() -> new CentralDogmaException("failed to initialize internal repositories"));
133             }
134         }
135     }
136 
137     @Nullable
138     private UserAndTimestamp metadataCreation() {
139         if (name.equals(INTERNAL_PROJECT_DOGMA)) {
140             return null;
141         }
142         final Entry<JsonNode> metadata = repos.get(REPO_DOGMA)
143                                               .get(Revision.HEAD, Query.ofJson(METADATA_JSON))
144                                               .join();
145         try {
146             return Jackson.treeToValue(metadata.content(), ProjectMetadata.class)
147                           .creation();
148         } catch (JsonParseException | JsonMappingException e) {
149             logger.warn("Failed to retrieve creation in {} file. project: {}", METADATA_JSON, name);
150             return null;
151         }
152     }
153 
154     private RepositoryManager newRepoManager(File rootDir, Executor repositoryWorker, Executor purgeWorker,
155                                              @Nullable RepositoryCache cache) {
156         // Enable caching if 'cache' is not null.
157         final GitRepositoryManager gitRepos =
158                 new GitRepositoryManager(this, rootDir, repositoryWorker, purgeWorker, cache);
159         return cache == null ? gitRepos : new CachingRepositoryManager(gitRepos, cache);
160     }
161 
162     private void createReservedRepos(long creationTimeMillis) {
163         if (!repos.exists(REPO_DOGMA)) {
164             try {
165                 repos.create(REPO_DOGMA, creationTimeMillis, Author.SYSTEM);
166             } catch (RepositoryExistsException ignored) {
167                 // Just in case there's a race.
168             }
169         }
170         if (!repos.exists(REPO_META)) {
171             try {
172                 repos.create(REPO_META, creationTimeMillis, Author.SYSTEM);
173             } catch (RepositoryExistsException ignored) {
174                 // Just in case there's a race.
175             }
176         }
177     }
178 
179     private void initializeMetadata(long creationTimeMillis, Author author) {
180         // Do not generate a metadata file for internal projects.
181         if (name.equals(INTERNAL_PROJECT_DOGMA)) {
182             return;
183         }
184 
185         final Repository dogmaRepo = repos.get(REPO_DOGMA);
186         final Revision headRev = dogmaRepo.normalizeNow(Revision.HEAD);
187         if (!dogmaRepo.exists(headRev, METADATA_JSON).join()) {
188             logger.info("Initializing metadata: {}", name);
189 
190             final UserAndTimestamp userAndTimestamp = UserAndTimestamp.of(author);
191             final RepositoryMetadata repo = new RepositoryMetadata(REPO_META, userAndTimestamp,
192                                                                    PerRolePermissions.ofInternal());
193             final Member member = new Member(author, ProjectRole.OWNER, userAndTimestamp);
194             final ProjectMetadata metadata = new ProjectMetadata(name,
195                                                                  ImmutableMap.of(repo.id(), repo),
196                                                                  ImmutableMap.of(member.id(), member),
197                                                                  ImmutableMap.of(),
198                                                                  userAndTimestamp, null);
199 
200             dogmaRepo.commit(headRev, creationTimeMillis, Author.SYSTEM,
201                              "Initialize metadata", "", Markup.PLAINTEXT,
202                              Change.ofJsonUpsert(METADATA_JSON, Jackson.valueToTree(metadata))).join();
203         }
204     }
205 
206     @Override
207     public String name() {
208         return name;
209     }
210 
211     @Override
212     public long creationTimeMillis() {
213         return creationTimeMillis;
214     }
215 
216     @Override
217     public Author author() {
218         return author;
219     }
220 
221     @Override
222     public MetaRepository metaRepo() {
223         MetaRepository metaRepo = this.metaRepo.get();
224         if (metaRepo != null) {
225             return metaRepo;
226         }
227 
228         metaRepo = new DefaultMetaRepository(repos.get(REPO_META));
229         if (this.metaRepo.compareAndSet(null, metaRepo)) {
230             return metaRepo;
231         } else {
232             return this.metaRepo.get();
233         }
234     }
235 
236     @Override
237     public RepositoryManager repos() {
238         return repos;
239     }
240 
241     @Override
242     public String toString() {
243         return Util.simpleTypeName(getClass()) + '(' + repos + ')';
244     }
245 }