source: src/main/java/geniusweb/partiesserver/AvailablePartiesUpdater.java@ 13

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

Initial Release

File size: 7.5 KB
Line 
1package geniusweb.partiesserver;
2
3import java.io.File;
4import java.io.IOException;
5import java.net.JarURLConnection;
6import java.net.MalformedURLException;
7import java.net.URISyntaxException;
8import java.net.URL;
9import java.net.URLClassLoader;
10import java.nio.file.Path;
11import java.nio.file.Paths;
12import java.util.Collection;
13import java.util.LinkedList;
14import java.util.logging.Level;
15
16import geniusweb.partiesserver.repository.AvailablePartiesRepo;
17import geniusweb.partiesserver.repository.AvailableParty;
18import geniusweb.party.Party;
19import tudelft.utilities.files.FileInfo;
20import tudelft.utilities.files.FileWatcher;
21import tudelft.utilities.immutablelist.Tuple;
22import tudelft.utilities.listener.Listenable;
23import tudelft.utilities.logging.ReportToLogger;
24import tudelft.utilities.logging.Reporter;
25import tudelft.utilities.repository.Repository;
26import java.util.jar.Attributes;
27
28/**
29 *
30 * This object keeps the {@link AvailablePartiesRepo} in sync with the file
31 * system. To work, this must be started in a global background thread,
32 * independent of current sessions etc. This thread is created when the
33 * constructor is called.
34 * <p>
35 * The {@link #PARTIESREPO} points to a directory that must exist. All the jar
36 * files in there are collected and tested if they can be loaded and are then
37 * added to the list of available parties.
38 * <p>
39 * Please make only 1 instance of this class. Double updates on the
40 * {@link AvailablePartiesRepo} is not useful
41 */
42public class AvailablePartiesUpdater {
43 private static final String PARTIESREPO = "partiesrepo";
44 private final Path rootdir;
45 private final Reporter log = new ReportToLogger("partiesserver");
46 private final Listenable<Tuple<FileInfo, FileInfo>> watcher;
47 private final Repository<String, AvailableParty> repository;
48
49 /**
50 *
51 * @param repo the repo to be automatically updated.
52 */
53 public AvailablePartiesUpdater(Repository<String, AvailableParty> repo) {
54 this.repository = repo;
55 this.rootdir = getRootDir();
56 log.log(Level.INFO, "Repository located at " + this.rootdir);
57 watcher = getFileWatcher(rootdir.toFile());
58 watcher.addListener(info -> update(info));
59 // manually trigger the first event.
60 update(new Tuple<FileInfo, FileInfo>(null,
61 new FileInfo(rootdir.toFile(), 2)));
62 }
63
64 /**
65 * Called when some file that may be relevant has changed. Synchronized to
66 * avoid weird states if filesystem changes rapidly. We ignore everything
67 * but jar files.
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
76 Path relpath = file.toPath().relativize(rootdir);
77 // workaround: root gives "" with depth 1 instead of 0.
78 if (relpath.toString().isEmpty()) {
79 reset();
80 return;
81 }
82
83 // it's file or dir inside the root directory
84 if (newfile == null) {
85 // file removed
86 repository.remove(getShortName(file));
87 } else {
88 tryLoad(file);
89 }
90
91 }
92
93 /**
94 * @param jarfile a jarfile
95 * @return the filename only, without extension
96 *
97 */
98 private String getShortName(File jarfile) {
99 String partyname = jarfile.getName();
100 return partyname.substring(0, partyname.length() - 4);
101
102 }
103
104 /**
105 * Try to load a file. Report to logger if it fails to load.
106 *
107 * @param file the file to load. Supposedly a jar file but we will report if
108 * not.
109 */
110 private void tryLoad(File file) {
111 try {
112 repository.put(new AvailableParty(getShortName(file), load(file)));
113 } catch (Throwable e) {
114 log.log(Level.WARNING, "Can not load class from file " + file, e);
115 }
116 }
117
118 /**
119 * Try to load jarfile, check that it contains required info and that the
120 * class can be instantiated.
121 *
122 * @param jarfile the jarfile to load the class from
123 * @return the loaded Class file from the jarfile (the class that is
124 * assigned as 'main class' in the manifest).
125 * @throws IOException
126 * @throws IllegalAccessException
127 * @throws InstantiationException
128 * @throws ClassNotFoundException
129 * @throws IllegalArgumentException if the given file is not a jar file or
130 * can't be read. Or if the file has no
131 * manifest containing a main class.
132 * @throws RuntimeException if party throws something during
133 * construction or while handling calls to
134 * it.
135 */
136 @SuppressWarnings("unchecked")
137 private Class<? extends Party> load(File jarfile)
138 throws IOException, InstantiationException, IllegalAccessException,
139 ClassNotFoundException {
140 /**
141 * We can not call loader.close() as instances of this class may need to
142 * load more later on when they start running. Only when all instances
143 * are dead and gone we could close but it's not clear how to track the
144 * number of active instances nor how to get a callback on that here.
145 */
146 JarClassLoader1 loader = new JarClassLoader1(toURL(jarfile),
147 getClass().getClassLoader());
148 String mainclass = loader.getMainClassName();
149 if (mainclass == null) {
150 throw new IllegalArgumentException(
151 "jar file does not contain manifest with main class");
152 }
153 Class<? extends Party> partyclass = (Class<? extends Party>) loader
154 .loadClass(mainclass);
155 partyclass.newInstance();
156 return partyclass;
157 }
158
159 private URL toURL(File file) throws MalformedURLException {
160 return new URL("jar:" + file.toURI().toURL() + "!/");
161 }
162
163 /**
164 * Remove all parties and reload them all from scratch
165 */
166 private void reset() {
167 Collection<AvailableParty> toremove = new LinkedList<>(
168 repository.list());
169 for (AvailableParty party : toremove) {
170 repository.remove(party.getID());
171 }
172 File rootfile = rootdir.toFile();
173 if (rootfile.exists() && rootfile.canRead() && rootfile.isDirectory()) {
174 for (File file : rootfile.listFiles()) {
175 tryLoad(file);
176 }
177 } else {
178 log.log(Level.SEVERE, "Can not read repository root directory "
179 + rootdir
180 + ": it does not exist, can not be read or is not a directory.");
181 }
182 }
183
184 /**
185 * Search from location of this class upwards till we find a directory
186 * called "domainsrepo".
187 *
188 * @return the root dir of the repo. This is the 'database' where all known
189 * profiles are stored.
190 */
191 protected Path getRootDir() {
192 // If you run this normal from tomcat we are somewhere inside
193 // projectroot/WEB_INF/classes/....
194 // Search back upwards to projectroot.
195 Path dir;
196 try {
197 dir = Paths.get(getClass().getProtectionDomain().getCodeSource()
198 .getLocation().toURI()).getParent();
199 } catch (URISyntaxException e) {
200 throw new RuntimeException("problem with path uri", e);
201 }
202 log.log(Level.INFO,
203 "searching for " + PARTIESREPO + " upwards from " + dir);
204 while (dir.getNameCount() > 1) {
205 Path repo = dir.resolve(PARTIESREPO);
206 if (repo.toFile().exists())
207 return repo;
208 log.log(Level.INFO, "Directory did not contain repo:" + dir);
209 dir = dir.getParent();
210 }
211 return Paths.get(PARTIESREPO); // bit silly, it's not there, but we need
212 // to do something.
213 }
214
215 /**
216 * Factory method that gives a watcher for file changes. See
217 * {@link FileWatcher}.
218 *
219 * @param directory the directory to watch
220 * @return a class that watches for file changes in the given directory.
221 * Should ignore changes in sub-directories
222 */
223 protected Listenable<Tuple<FileInfo, FileInfo>> getFileWatcher(
224 File directory) {
225 return new FileWatcher(directory, 3000, 1);
226 }
227}
228
229
Note: See TracBrowser for help on using the repository browser.