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

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

Initial Release

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