View Javadoc

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  	// private static final Logger LOGGER = Logger.getLogger(YamlFilesystemStorageService.class);
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  		// TODO : do this smarter
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 	// HELPER METHODS
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 }