source: src/main/java/geniusweb/profilesserver/AutoUpdatingProfilesRepository.java@ 30

Last change on this file since 30 was 30, checked in by bart, 3 years ago

Fixed windows time-out.

File size: 12.8 KB
Line 
1package geniusweb.profilesserver;
2
3import java.io.File;
4import java.io.FileWriter;
5import java.io.IOException;
6import java.io.Writer;
7import java.net.URISyntaxException;
8import java.nio.file.Path;
9import java.nio.file.Paths;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.logging.Level;
13
14import geniusweb.issuevalue.Domain;
15import geniusweb.profile.Profile;
16import tudelft.utilities.files.FileInfo;
17import tudelft.utilities.files.FileWatcher;
18import tudelft.utilities.immutablelist.Tuple;
19import tudelft.utilities.listener.Listenable;
20import tudelft.utilities.listener.Listener;
21import tudelft.utilities.logging.Reporter;
22
23/**
24 * As {@link DefaultProfilesRepository}, but auto synchronizes the contents of
25 * the repository according to changes in the filesystem. Domain and profile
26 * contents should match their directoryname/filename. A directory can only
27 * contain files for the domain with the same name as the directoryname.
28 *
29 * This repository tries to use the value in the environment variable
30 * "PROFILES_ROOT" as base dir for the available profiles. If it does not exist,
31 * it assumes "repo" in the current working directory. It is assumed ot contain
32 * a directory for each domain, with the name of the directory matching the
33 * {@link Domain#getName()}. We recomend to set an absolute path to
34 * PROFILES_ROOT to avoid problems if something changes the working directory.
35 * <p>
36 * Thread safe. It is heavily synchronized to ensure thread safety. It does not
37 * manipulate the filesystem to avoid collisions between trackers and updaters.
38 * <p>
39 * Tech note: This is not a singleton because we need to override functions for
40 * testing purposes.
41 */
42public class AutoUpdatingProfilesRepository extends DefaultProfilesRepository {
43
44 private static final String PROFILES_ROOTDIR = "PROFILES_ROOTDIR";
45 private static final String DOMAINSREPO = "domainsrepo";
46 private static final String JSON = ".json";
47 private final Listenable<Tuple<FileInfo, FileInfo>> watcher;
48 private Path rootdir;
49 private final Reporter log;
50
51 public AutoUpdatingProfilesRepository(Reporter logger) {
52 this.log = logger;
53 this.rootdir = getRootDir();
54
55 watcher = getFileWatcher(rootdir.toFile());
56 watcher.addListener(new Listener<Tuple<FileInfo, FileInfo>>() {
57 @Override
58 public void notifyChange(Tuple<FileInfo, FileInfo> info) {
59 update(info);
60 }
61 });
62 // manually trigger the first event.
63 update(new Tuple<FileInfo, FileInfo>(null,
64 new FileInfo(rootdir.toFile(), 2)));
65 }
66
67 @Override
68 public synchronized void putProfile(Profile profile) throws IOException {
69 if (!available.containsKey(profile.getDomain())) {
70 throw new IOException("profile's domain does not exist");
71 }
72 String domainname = profile.getDomain().getName();
73 String profilename = profile.getName();
74 if (!profilename.matches("[a-zA-Z0-9-]+"))
75 throw new IOException("illegal profile name " + profilename
76 + ", only letters, numbers and - allowed");
77 File file = Paths
78 .get(rootdir.toString(), domainname, profilename + JSON)
79 .toFile();
80 if (file.exists()) {
81 String msg = fileCheck(file, false, true);
82 if (msg != null)
83 throw new IOException(msg);
84 }
85
86 // CHECK utf-8 compatibility? Do we need that anyway?
87 try (Writer out = new FileWriter(file, false)) {
88 out.write(Jackson.instance().writeValueAsString(profile));
89 }
90 // trigger the update immediately, avoid wait for FileChecker.
91 super.add(profile);
92
93 }
94
95 /**
96 * Called when some file that may be relevant has changed. Synchronized to
97 * avoid weird states if filesystem changes rapidly.
98 */
99 private synchronized void update(Tuple<FileInfo, FileInfo> info) {
100 log.log(Level.INFO, "change: " + info);
101 File oldfile = info.get1() == null ? null : info.get1().getFile();
102 File newfile = info.get2() == null ? null : info.get2().getFile();
103
104 File file = oldfile != null ? oldfile : newfile;
105 Path relpath = file.toPath().relativize(rootdir);
106 // workaround: root gives "" with depth 1 instead of 0.
107 int depth = relpath.toString().isEmpty() ? 0 : relpath.getNameCount();
108
109 switch (depth) {
110 case 0:
111 // root changed! Update ALL directories we saw and see.
112 reset();
113 break;
114 case 1: // change of directory name?
115 if (newfile != null) {
116 if (newfile.isDirectory()) {
117 updateDomainDirectory(newfile);
118 } else {
119 log.log(Level.INFO, "expected directory but found file: "
120 + newfile.getAbsolutePath());
121 }
122 } else {
123 // new file is null. Removed
124 remove(getDomain(oldfile.getName()));
125 }
126 break;
127 case 2: // A file changed, added or removed inside profile directory
128 if (newfile != null) {
129 // file was added.
130 if (newfile.isDirectory()) {
131 log.log(Level.INFO,
132 "expected file but found directory: " + file);
133 }
134 updateDomainDirectory(newfile.getParentFile());
135 } else {
136 // Some file was removed. Check entire directory again
137 updateDomainDirectory(oldfile.getParentFile());
138 }
139 break;
140 default:
141 log.log(Level.WARNING, "Ignoring unexpected event at " + info);
142 }
143
144 }
145
146 /**
147 * Remove all domains and reload them all from scratch
148 */
149 private void reset() {
150 // first remove all old stuff. Copy domains as we're going to modify.
151 for (Domain domain : new LinkedList<>(available.keySet())) {
152 remove(domain);
153 }
154 if (!basicFileCheck(rootdir.toFile(), true))
155 return;
156 for (File dir : rootdir.toFile().listFiles()) {
157 if (!dir.isDirectory())
158 continue;
159 updateDomainDirectory(dir);
160 }
161 }
162
163 /**
164 * A domain directory was added or updated (not deleted). Scan the contents
165 * and compare/update the repo.
166 *
167 * @param dir the directory that could contain domain specific files -
168 * domain description and profiles.
169 */
170 private void updateDomainDirectory(File dir) {
171 if (!basicFileCheck(dir, true)) {
172 remove(getDomain(dir.getName()));
173 return;
174 }
175 // if we get here, dir is indeed a directory and we can read contents.
176 File domainfile = domainFile(dir.getName());
177 Domain newdomain = deserializeDomain(domainfile);
178 Domain olddomain = getDomain(dir.getName());
179 if (olddomain != null && !olddomain.equals(newdomain)) {
180 remove(olddomain); // also removes old profiles.
181 }
182
183 if (newdomain == null) {
184 return;
185 }
186 add(newdomain);
187 updateProfiles(newdomain);
188 }
189
190 /**
191 * Update all profiles in given directory
192 *
193 * @param dir
194 */
195 private void updateProfiles(Domain domain) {
196 final Path domaindir = rootdir.resolve(domain.getName());
197 final File domainfile = domainFile(domain.getName());
198 final List<Profile> newprofiles = new LinkedList<>();
199 for (File file : domaindir.toFile().listFiles()) {
200 if (file.equals(domainfile))
201 continue;
202 Profile newprofile = deserializeProfile(file);
203 if (newprofile == null)
204 continue;
205 if (!newprofile.getDomain().equals(domain)) {
206 log.log(Level.WARNING,
207 "Profile " + file + " contains incorrect domain "
208 + newprofile.getDomain());
209 continue;
210 }
211 if (!newprofile.getName()
212 .equals(withoutExtension(file.getName()))) {
213 log.log(Level.WARNING, "Profile " + file
214 + " contains incorrect name " + newprofile.getName());
215 continue;
216 }
217 newprofiles.add(newprofile);
218 }
219 // remove profiles not on the filesystem anymore/have errors
220 for (Profile profile : getProfiles(domain.getName())) {
221 if (!newprofiles.contains(profile)) {
222 remove(profile);
223 }
224 }
225 // (re)add currently existing profiles.
226 for (Profile profile : newprofiles) {
227 add(profile);
228 }
229 }
230
231 /**
232 * Reads profile and also checks that this profile is in the correct
233 * directory (the domain should match the registered domain for this
234 * directory). The domain for this directory MUST have been registered.
235 *
236 * @param profilefiile the profile file to read. Should contain a
237 * {@link Profile}.
238 * @return profile, or null if failed to read. If failed, we log the
239 * details.
240 */
241
242 private Profile deserializeProfile(File profilefile) {
243 if (!basicFileCheck(profilefile, false))
244 return null;
245 Profile profile = null;
246 try {
247 profile = Jackson.instance().readValue(profilefile, Profile.class);
248 } catch (IOException e) {
249 log.log(Level.WARNING, "File " + profilefile
250 + " does not appear to contain a profile", e);
251 return null;
252 }
253 Domain expectedDomain = getDomain(
254 profilefile.getParentFile().getName());
255 if (!profile.getDomain().equals(expectedDomain)) {
256 log.log(Level.WARNING, "Profile has incorrect domain: expected "
257 + expectedDomain + " but found " + profile.getDomain());
258 return null;
259
260 }
261 return profile;
262 }
263
264 /**
265 * Try to deserialize a domain file in given directory.
266 *
267 * @param dir the directory where we expect a domain file
268 * @return deserialized domain file, or null if deserialization fails (eg no
269 * such file, or wrong contents, domain has name not matching the
270 * actual file). If fails, a warning is logged with details.
271 *
272 *
273 */
274 private Domain deserializeDomain(File domainfile) {
275 if (!basicFileCheck(domainfile, false))
276 return null;
277 Domain domain = null;
278 String expectedName = withoutExtension(domainfile.getName());
279 try {
280 domain = Jackson.instance().readValue(domainfile, Domain.class);
281 } catch (IOException e) {
282 log.log(Level.WARNING, "File " + domainfile
283 + " does not appear to contain a Domain", e);
284 return null;
285 }
286 if (!expectedName.equals(domain.getName())) {
287 log.log(Level.WARNING,
288 "domain file " + domainfile + " contains name:'"
289 + domain.getName()
290 + "' which does not match the required name "
291 + expectedName);
292 return null;
293 }
294 return domain;
295
296 }
297
298 /**
299 * Silent check and report basic problems with a file. Silent in the sense
300 * that errors are logged but cause no exception.
301 *
302 * @param file the file to check
303 * @param shouldbeDirectory true if the file should be a directory
304 * @return false iff basic checks on the file fails.
305 *
306 */
307 private boolean basicFileCheck(File file, boolean shouldbeDirectory) {
308 String msg = fileCheck(file, shouldbeDirectory, false);
309 if (msg != null) {
310 log.log(Level.WARNING, msg);
311 return false;
312 }
313 return true;
314 }
315
316 /**
317 *
318 * @param file the file to check
319 * @param shouldbeDirectory true if expected a directory
320 * @param shouldbeWritable true if expected to be writable
321 * @return null if file is there, is file/directory, allows reading and
322 * allows writing (if specified).
323 */
324 private String fileCheck(File file, boolean shouldbeDirectory,
325 boolean shouldbeWritable) {
326 if (!file.exists()) {
327 return file + " is missing";
328 }
329 if (!file.canRead()) {
330 return "directory " + file + "does not allow reading ";
331 }
332 if (shouldbeDirectory) {
333 if (!file.isDirectory()) {
334 return file + " is not a directory";
335 }
336 } else {
337 if (!file.isFile()) {
338 return file + " is not a file";
339 }
340 }
341 if (shouldbeWritable && !file.canWrite()) {
342 return file + "is read-only";
343 }
344 return null;
345
346 }
347
348 private String withoutExtension(String name) {
349 if (!name.contains("."))
350 return name;
351 return name.substring(0, name.indexOf("."));
352 }
353
354 /**
355 * @param directory a directory that might contain a domain file.
356 * @return the file that contains the domain description in the given
357 * directory.
358 *
359 */
360 private File domainFile(String domainname) {
361 return Paths.get(rootdir.toString(), domainname, domainname + JSON)
362 .toFile();
363 }
364
365 /**
366 * Factory method that gives a watcher for file changes. See
367 * {@link FileWatcher}.
368 *
369 * @param file the file/dir to watch
370 * @return a class that watches for file changes below the given file.
371 */
372 protected Listenable<Tuple<FileInfo, FileInfo>> getFileWatcher(File file) {
373 return new FileWatcher(file, 3000, 2);
374 }
375
376 /**
377 * Search from location of this class upwards till we find a directory
378 * called "domainsrepo".
379 *
380 * @return the root dir of the repo. This is the 'database' where all known
381 * profiles are stored.
382 */
383 protected Path getRootDir() {
384 // If you run this normal from tomcat we are somewhere inside
385 // projectroot/WEB_INF/classes/....
386 // Search back upwards to projectroot.
387 Path dir;
388 String path = System.getenv(PROFILES_ROOTDIR);
389 if (path != null) {
390 dir = Paths.get(path);
391 if (!dir.toFile().exists())
392 throw new RuntimeException("Hard set path PROFILES_ROOTDIR='"
393 + PROFILES_ROOTDIR + "' does not exist ");
394 return dir;
395 }
396 try {
397 dir = Paths.get(getClass().getProtectionDomain().getCodeSource()
398 .getLocation().toURI()).getParent();
399 } catch (URISyntaxException e) {
400 throw new RuntimeException("Problem with path URI", e);
401 }
402 System.out.println("searching from " + dir);
403 while (dir.getNameCount() > 1) {
404 Path repo = dir.resolve(DOMAINSREPO);
405 if (repo.toFile().exists())
406 return repo;
407 System.out.println("Directory did not contain repo:" + dir);
408 dir = dir.getParent();
409 }
410 throw new RuntimeException("Path to " + DOMAINSREPO + " was not found");
411 }
412
413}
Note: See TracBrowser for help on using the repository browser.