1 /***
2 * Copyright (C) 2008 Jeremy Thomerson (jthomerson@users.sourceforge.net)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package net.sf.vcaperture.services;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.Iterator;
25 import java.util.List;
26
27 import net.sf.vcaperture.model.AbstractRepository;
28 import net.sf.vcaperture.model.IApplicationContext;
29 import net.sf.vcaperture.model.IDataRetriever;
30 import net.sf.vcaperture.model.RepoFile;
31 import net.sf.vcaperture.model.RepoFileRevision;
32 import net.sf.vcaperture.model.Revision;
33 import net.sf.vcaperture.util.BeanUtil;
34 import net.sf.vcaperture.util.RevisionUtil;
35 import net.sf.vcaperture.util.spring.ApplicationContextFactory;
36
37 import org.apache.commons.io.FileUtils;
38 import org.apache.commons.io.filefilter.FalseFileFilter;
39 import org.apache.commons.io.filefilter.NameFileFilter;
40 import org.apache.commons.io.filefilter.SuffixFileFilter;
41 import org.apache.commons.io.filefilter.TrueFileFilter;
42 import org.apache.commons.lang.StringUtils;
43
44 /***
45 * Version of <tt>ILocalStorageService</tt> that uses a local filesystem directory structure and
46 * YAML files to store information about the repositories.
47 *
48 * <pre>
49 * INTERNAL IMPLEMENTATION DETAIL (SHOULD NOT BE USED BY OUTSIDE DEVELOPERS / SUBJECT TO CHANGE)
50 * Directory Structure:
51 * repoName
52 * revisions (DIRECTORY)
53 * latest-version.txt
54 * {REVISION-GROUP-FOLDER} - Break revisions into chunks so that there are not millions of subfolders
55 * {REVISION}
56 * revision.yml
57 * file/dir/structure/{FILENAME} - contents
58 * files
59 * file/dir/structure/${FILENAME}.yml
60 * </pre>
61 *
62 * <p>
63 * TODO: This is probably not the most highly efficient implementation possible. I guess that in
64 * practice, especially in use from the web app, it will be fairly slow due to the large number of
65 * file operations it must do. But it was quick and easy to put together. I am thinking that a
66 * better implementation might be entirely based on Lucene.
67 * </p>
68 *
69 * @author Jeremy Thomerson (jthomerson@users.sourceforge.net)
70 */
71 public class YamlFilesystemStorageService implements ILocalStorageService {
72
73
74 private static final String LATEST_VERSION_FILE = "revisions/latest-version.txt";
75 private static final String REVISION_DATA_FILE = "revision.yml";
76 private File mStorageDirectory;
77
78 public Collection<Revision> getRevisions(AbstractRepository repo, String startingRevision, int maxRevisions) {
79 File file = new File(getRepoStorageDirectory(repo), "revisions");
80 List<Revision> revs = new ArrayList<Revision>();
81 Iterator it = FileUtils.iterateFiles(file, new NameFileFilter(REVISION_DATA_FILE), TrueFileFilter.INSTANCE);
82
83 List<File> files = new ArrayList<File>();
84 while (it.hasNext()) {
85 files.add((File) it.next());
86 }
87 for (Iterator<File> fileIT = files.iterator(); fileIT.hasNext();) {
88 File revFile = fileIT.next();
89 Revision rev = BeanUtil.loadType(revFile, Revision.class);
90 if (repo.isRevisionNewer(rev.getName(), startingRevision)) {
91 for (RepoFileRevision rfr : rev.getFiles()) {
92 rfr.setContentsRetriever(new YamlFilesystemStorageContentsRetriever(repo));
93 }
94 revs.add(rev);
95 }
96 }
97 RevisionUtil.sort(repo, revs);
98 return revs;
99 }
100
101 protected String getContents(AbstractRepository repo, RepoFileRevision rfr) {
102 if (rfr.isDirectory()) {
103 return null;
104 }
105 File contents = new File(getRevisionFolder(repo, rfr.getRevision()), rfr.getRelativePath());
106 try {
107 return FileUtils.readFileToString(contents);
108 } catch (IOException ioe) {
109 throw new StorageException("Error loading file contents: " + ioe.getMessage(), ioe);
110 }
111 }
112
113 public String getLatestStoredRevision(AbstractRepository repo) {
114 File file = new File(getRepoStorageDirectory(repo), LATEST_VERSION_FILE);
115 if (file.exists() && file.isFile()) {
116 try {
117 String rev = FileUtils.readFileToString(file);
118 return rev == null ? repo.getNullRevisionDefault() : rev.trim();
119 } catch (IOException ioe) {
120 throw new StorageException("Error in storage mechanism: " + ioe.getMessage(), ioe);
121 }
122 }
123 return repo.getNullRevisionDefault();
124 }
125
126 public void updateLatestStoredRevision(AbstractRepository repo, Revision rev) {
127 File file = new File(getRepoStorageDirectory(repo), LATEST_VERSION_FILE);
128 try {
129 FileUtils.writeStringToFile(file, rev.getName());
130 } catch (IOException ioe) {
131 throw new StorageException("Error in storage mechanism: " + ioe.getMessage(), ioe);
132 }
133 }
134
135 public RepoFile getFile(AbstractRepository repo, String relativePath) {
136 if (StringUtils.isEmpty(relativePath)) {
137 return new YamlFilesystemRepoFile(repo, "/", true);
138 }
139 File file = new File(getFilesStorageDirectory(repo), relativePath + ".yml");
140 if (file.exists()) {
141 YamlFilesystemRepoFile rf = BeanUtil.loadType(file, YamlFilesystemRepoFile.class);
142 rf.setRepository(repo);
143 return rf;
144 }
145 return null;
146 }
147
148 public List<RepoFile> getChildren(AbstractRepository repo, RepoFile repoFile, boolean showAll) {
149 File dir = new File(getFilesStorageDirectory(repo), repoFile.getRelativePath());
150 if (!dir.isDirectory()) {
151 return Collections.emptyList();
152 }
153 List<RepoFile> children = new ArrayList<RepoFile>();
154 Iterator it = FileUtils.iterateFiles(dir, new SuffixFileFilter(".yml"), FalseFileFilter.INSTANCE);
155 while (it.hasNext()) {
156 File childFile = (File) it.next();
157 RepoFile rf = BeanUtil.loadType(childFile, RepoFile.class);
158 if (showAll || rf.isDirectory()) {
159 children.add(rf);
160 }
161 }
162
163 return children;
164 }
165
166 public void saveRevision(AbstractRepository repo, Revision rev) {
167 File file = new File(getRevisionFolder(repo, rev.getName()), REVISION_DATA_FILE);
168 BeanUtil.save(file, rev);
169 updateLatestStoredRevision(repo, rev);
170 }
171
172 public void saveFileRevision(AbstractRepository repo, RepoFileRevision rfr) {
173 File contents = new File(getRevisionFolder(repo, rfr.getRevision()), rfr.getRelativePath());
174 contents.getParentFile().mkdirs();
175
176 File fileInfo = new File(getFilesStorageDirectory(repo), rfr.getRelativePath() + ".yml");
177 fileInfo.getParentFile().mkdirs();
178 try {
179 if (!rfr.isDirectory()) {
180 FileUtils.writeStringToFile(contents, rfr.getContents());
181 }
182 BeanUtil.save(fileInfo, new RepoFile(rfr));
183 } catch (IOException ioe) {
184 throw new StorageException("Error saving file revision: " + ioe.getMessage(), ioe);
185 }
186 }
187
188 public File getStorageDirectory() {
189 return mStorageDirectory;
190 }
191
192 public void setStorageDirectory(File storageDirectory) {
193 mStorageDirectory = storageDirectory;
194 }
195
196
197 private File getRepoStorageDirectory(AbstractRepository repo) {
198 File file = new File(mStorageDirectory, repo.getName());
199 file.mkdirs();
200 return file;
201 }
202
203 private File getFilesStorageDirectory(AbstractRepository repo) {
204 File file = new File(getRepoStorageDirectory(repo), "files");
205 file.mkdirs();
206 return file;
207 }
208
209 private File getRevisionFolder(AbstractRepository repo, String rev) {
210 File file = new File(getRepoStorageDirectory(repo), getFolderString(getRevisionNumber(rev)));
211 file.mkdirs();
212 return file;
213 }
214
215 private int getRevisionNumber(String rev) {
216 try {
217 return Integer.parseInt(rev);
218 } catch (NumberFormatException nfe) {
219 throw new UnsupportedOperationException("Need to come up with a plan for storing non-numeric revisions.");
220 }
221 }
222
223 private static String getFolderString(int rev) {
224 String str = Integer.toString(rev);
225 String folder = "";
226 int beg = 0;
227 while ((beg + 2) <= str.length()) {
228 int end = beg + 2;
229 String dir = str.substring(beg, end);
230 folder += (dir + "/");
231 beg += 2;
232 }
233 int remaining = Math.min(str.length(), beg + 2);
234 if ((remaining - beg) > 0) {
235 folder += (str.substring(beg, remaining) + "/");
236 }
237 return "revisions/" + folder + rev + "/";
238 }
239
240 static class YamlFilesystemRepoFile extends RepoFile {
241
242 private AbstractRepository mRepository;
243
244 public YamlFilesystemRepoFile() {
245 }
246 public YamlFilesystemRepoFile(AbstractRepository repo, String relativePath, boolean directory) {
247 mRepository = repo;
248 setRelativePath(relativePath);
249 setDirectory(directory);
250 }
251
252 public void setRepository(AbstractRepository repository) {
253 mRepository = repository;
254 }
255
256 @Override
257 public List<RepoFile> getChildren(boolean showAll) {
258 IApplicationContext context = ApplicationContextFactory.getFactory().getContext();
259 YamlFilesystemStorageService svc = (YamlFilesystemStorageService) context.getBean(ILocalStorageService.class);
260 return svc.getChildren(mRepository, this, showAll);
261 }
262
263 }
264
265 static class YamlFilesystemStorageContentsRetriever implements IDataRetriever<RepoFileRevision, String> {
266 private final AbstractRepository mRepository;
267
268 YamlFilesystemStorageContentsRetriever(AbstractRepository repo) {
269 mRepository = repo;
270 }
271
272 public String getData(RepoFileRevision key) {
273 IApplicationContext context = ApplicationContextFactory.getFactory().getContext();
274 YamlFilesystemStorageService svc = (YamlFilesystemStorageService) context.getBean(ILocalStorageService.class);
275 return svc.getContents(mRepository, key);
276 }
277
278 }
279
280 }