source: src/main/java/geniusweb/profilesserver/AutoUpdatingProfilesFactory.java@ 10

Last change on this file since 10 was 8, checked in by bart, 5 years ago

Update 28 jan 2020

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