1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.io;
21
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.io.ObjectInputStream;
29 import java.io.ObjectOutputStream;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.HashSet;
33 import java.util.Hashtable;
34 import java.util.Iterator;
35 import java.util.Vector;
36
37 import org.apache.log4j.Logger;
38 import org.xnap.XNap;
39 import org.xnap.event.ListListener;
40 import org.xnap.event.ListSupport;
41 import org.xnap.event.StatusListener;
42 import org.xnap.util.FileHelper;
43 import org.xnap.util.Preferences;
44 import org.xnap.util.SearchTree;
45
46 public class Library implements PropertyChangeListener {
47
48
49
50 /***
51 * The name of the file where the library is stored.
52 */
53 public static String FILENAME = FileHelper.getHomeDir() + "library";
54
55 /***
56 * The current version of the library.
57 */
58 public static int VERSION = 1;
59
60
61
62 private static Logger logger = Logger.getLogger(Library.class);
63 private static Library singleton = new Library();
64
65 private Preferences prefs = Preferences.getInstance();
66
67 /***
68 * Maps absolute filename to repository index.
69 */
70 private Hashtable indexByFile = new Hashtable();
71
72 /***
73 * Contains all files that have been deleted from disk.
74 */
75 private HashSet deletedFiles;
76
77 private int totalCount = 0;
78 private int sharedCount = 0;
79 private Thread runner;
80 private Object updateLock = new Object();
81 private boolean updatePending = false;
82 private boolean updateRunning = false;
83
84 private ArrayList files = new ArrayList();
85
86 private SearchTree st = new SearchTree();
87
88 private Vector miProviders = new Vector();
89 private StatusListener listener;
90 private ListSupport listeners = new ListSupport(this);
91
92 private boolean isRead = false;
93
94
95
96 public Library()
97 {
98 prefs.addPropertyChangeListener("libraryDirs", this);
99 prefs.addPropertyChangeListener("uploadDirs", this);
100
101 updateLater(true);
102 }
103
104
105
106 public static Library getInstance()
107 {
108 return singleton;
109 }
110
111 /***
112 * <code>Listener</code> is notified when a new file is
113 * added.
114 */
115 public void addListListener(ListListener listener)
116 {
117 listeners.addListListener(listener);
118 }
119
120 /***
121 * <code>Provider</code> is notified when a file is added or not uptodate.
122 */
123 public void addMetaInfoProvider(MetaInfoProvider provider)
124 {
125 miProviders.add(provider);
126 }
127
128 /***
129 * Returns a file by index. It is save to call this function with
130 * index <= size() because the library never shrinks.
131 *
132 * @return null, if the file was removed from library; the file
133 * located at index, otherwise */
134 public synchronized MetaInfoFile get(int index)
135 {
136 return (MetaInfoFile)files.get(index);
137 }
138
139 /***
140 * Returns a MetaInfoFile object if file is part of library.
141 *
142 * @return null, if the file is not part of library; the file,
143 * otherwise
144 */
145 public synchronized MetaInfoFile get(File file)
146 {
147 int i = indexOf(file);
148 return (i != -1) ? get(i) : null;
149 }
150
151 /***
152 * Returns file at <code>index</code> and compares it to the given
153 * filename. If filenames match returns the file.
154 *
155 * @return null, if file was not found; the file, otherwise */
156 public synchronized MetaInfoFile get(String filename, int index)
157 {
158 if (index >= 0 && index < files.size()) {
159 MetaInfoFile file = (MetaInfoFile)get(index);
160
161 if (file != null && file.getName().equals(filename)) {
162 return file;
163 }
164 }
165
166 return null;
167 }
168
169 public synchronized int indexOf(File file)
170 {
171 Integer i = (Integer)indexByFile.get(file);
172 return (i != null) ? i.intValue() : -1;
173 }
174
175 /***
176 * Returns true, if file is part of repository and shared. */
177 public synchronized boolean isShared(File file)
178 {
179 int index = indexOf(file);
180 return (index != -1) ? ((MetaInfoFile)get(index)).isShared() : false;
181 }
182
183 public boolean isRead()
184 {
185 return isRead;
186 }
187
188 /***
189 * Returns false, if <code>file</code> is the incomplete folder
190 * or its name starts with a dot.
191 */
192 public boolean isPartOfRepository(File file)
193 {
194 if (file.getName().startsWith(".")) {
195 return false;
196 }
197 else if (file.isDirectory()) {
198 return !file.equals(new File(prefs.getIncompleteDir()));
199 }
200
201 return true;
202 }
203
204 public boolean isUpdateRunning()
205 {
206 synchronized (updateLock) {
207 return updateRunning;
208 }
209 }
210
211 public void propertyChange(PropertyChangeEvent e)
212 {
213 updateLater();
214 }
215
216 /***
217 * Removes <code>listener</code>.
218 */
219 public void removeListListener(ListListener listener)
220 {
221 listeners.removeListListener(listener);
222 }
223
224 /***
225 * Removes <code>provider</code>.
226 */
227 public void removeMetaInfoProvider(MetaInfoProvider provider)
228 {
229 miProviders.remove(provider);
230 }
231
232 /***
233 * @return always returns a valid array
234 */
235 public MetaInfoFile[] search(String searchText)
236 {
237 if (st == null) {
238 return new MetaInfoFile[0];
239 }
240
241 HashSet results = st.search(searchText);
242 return (results != null)
243 ? (MetaInfoFile[])results.toArray(new MetaInfoFile[0])
244 : null;
245 }
246
247 public File[] search(String searchText, long filesize)
248 {
249 if (st == null) {
250 return new File[0];
251 }
252
253 HashSet results = st.search(searchText);
254 if (results != null) {
255 for (Iterator i = results.iterator(); i.hasNext();) {
256 if (((File)i.next()).length() != filesize) {
257 i.remove();
258 }
259 }
260 return (File[])results.toArray(new File[0]);
261 }
262
263 return null;
264 }
265
266 public synchronized int size()
267 {
268 return files.size();
269 }
270
271 /***
272 * Updates the repository.
273 */
274 public void updateLater()
275 {
276 updateLater(false);
277 }
278
279 public synchronized void setStatusListener(StatusListener newValue)
280 {
281 listener = newValue;
282 updateStatus();
283 }
284
285 public synchronized void updateStatus()
286 {
287 if (listener != null) {
288 listener.setStatus(XNap.tr("{0} of {1} {2}",
289 new Integer(sharedCount),
290 new Integer(totalCount),
291 (isUpdateRunning())
292 ? XNap.tr("updating")
293 : XNap.tr("shared")));
294 }
295 }
296
297 /***
298 * Optionaly reads the repository and then always updates the repository.
299 */
300 private void updateLater(boolean read)
301 {
302 updatePending = true;
303
304 synchronized (updateLock) {
305 if (updateRunning) {
306 return;
307 }
308
309 updateRunning = true;
310 }
311
312 runner = new Thread(new UpdateRunner(read), "UpdateRepository");
313 runner.start();
314 }
315
316 private void read()
317 {
318 logger.debug("reading library from " + FILENAME);
319
320 ObjectInputStream in = null;
321 try {
322 in = new ObjectInputStream(new FileInputStream(FILENAME));
323
324 int v = in.readInt();
325 logger.debug("current version: " + VERSION
326 + ", repository version: " + v);
327 if (v < VERSION) {
328 logger.debug("discarding old repository");
329 return;
330 }
331
332 int size = in.readInt();
333 if (size >= 0) {
334 logger.debug("allocating " + size + " items");
335 files.ensureCapacity(size);
336
337 for (int i = 0; i < size; i++) {
338 try {
339 Object o = in.readObject();
340 if (o != null && o instanceof MetaInfoFile) {
341 MetaInfoFile file = (MetaInfoFile)o;
342 addFile(file, file.isShared());
343 }
344 }
345 catch (IOException e) {
346 throw(e);
347 }
348 catch (Exception e) {
349 logger.error("error reading library at item "
350 + i + "/" + size, e);
351 }
352 }
353 }
354 }
355 catch(IOException e) {
356 logger.debug("error reading library file", e);
357 logger.info(XNap.tr("Could not read library file {0}.", FILENAME));
358 }
359 finally {
360 if (in != null) {
361 try {
362 in.close();
363 }
364 catch(IOException e) {
365 }
366 }
367 }
368 }
369
370 /***
371 * Writes repository contents to disk.
372 */
373 private void write()
374 {
375 logger.debug("writing library to " + FILENAME);
376
377 ObjectOutputStream out = null;
378 try {
379 out = new ObjectOutputStream(new FileOutputStream(FILENAME));
380
381 out.writeInt(VERSION);
382 out.writeInt(totalCount);
383
384 for (Iterator i = files.iterator(); i.hasNext();) {
385 Object o = i.next();
386 if (o != null) {
387 out.writeObject(o);
388 }
389 }
390 }
391 catch(IOException e) {
392 logger.debug("error writing library file", e);
393 logger.info(XNap.tr("Could not write library file {0}.", FILENAME));
394 }
395 finally {
396 if (out != null) {
397 try {
398 out.close();
399 }
400 catch(IOException e) {
401 }
402 }
403 }
404 }
405
406 /***
407 * Adds new files, removes old files and writes the repository to disk.
408 */
409 private void update()
410 {
411 logger.debug("updating library");
412
413 deletedFiles = new HashSet();
414
415
416 for (Iterator i = files.iterator(); i.hasNext();) {
417 File f = (File)i.next();
418 if (f != null) {
419 deletedFiles.add(f);
420 }
421 }
422
423 String[] sharedDirs = prefs.getUploadDirs();
424 HashSet sharedDirsSet = new HashSet(Arrays.asList(sharedDirs));
425
426
427 String[] dirs = prefs.getLibraryDirs();
428 for (int i = 0; i < dirs.length; i++) {
429 addDirectory(new File(dirs[i]), false, sharedDirsSet);
430 }
431
432
433 for (int i = 0; i < sharedDirs.length; i++) {
434 addDirectory(new File(sharedDirs[i]), true, null);
435 }
436
437 for (Iterator i = deletedFiles.iterator(); i.hasNext();) {
438 removeFile((MetaInfoFile)i.next());
439 }
440
441 deletedFiles = null;
442
443 write();
444 }
445
446 /***
447 * Adds <code>file</code> recursively.
448 *
449 * <p>Only invoked from {@link #update()}.
450 */
451 private void addDirectory(File file, boolean shared,
452 HashSet excludeDirs)
453 {
454 if ((excludeDirs != null
455 && excludeDirs.contains(file.getAbsolutePath()))
456 || !isPartOfRepository(file)) {
457 return;
458 }
459
460 if (file.isDirectory()) {
461 File list[] = file.listFiles();
462 if (list != null) {
463 for (int i = 0; i < list.length; i++) {
464 addDirectory(list[i], shared, excludeDirs);
465 }
466 }
467 }
468 else if (file.isFile() && file.canRead()) {
469 addFile(file, shared);
470 }
471 }
472
473 /***
474 * <p>Only invoked from {@link #addDirectory(File, boolean, HashSet)}.
475 */
476 private void addFile(File file, boolean shared)
477 {
478 int index = indexOf(file);
479 if (index == -1) {
480 if (!(file instanceof MetaInfoFile)) {
481 file = new MetaInfoFile(file);
482 }
483
484
485 updateFile((MetaInfoFile)file, shared);
486
487
488 files.add(file);
489 indexByFile.put(file, new Integer(files.size() - 1));
490 totalCount++;
491 if (shared) {
492 sharedCount++;
493 }
494
495 listeners.fireItemAdded(file);
496
497 if (totalCount % 16 == 0) {
498 updateStatus();
499 }
500
501 if (st != null) {
502 st.add(file.getAbsolutePath(), file);
503 }
504 }
505 else {
506
507 updateFile(get(index), shared);
508 }
509 }
510
511 /***
512 * Adds file if not already present.
513 *
514 * <p>Only invoked from {@link #addFile(File,boolean)}.
515 */
516 private void updateFile(MetaInfoFile file, boolean shared)
517 {
518 if (deletedFiles != null) {
519 deletedFiles.remove(file);
520 }
521
522 file.setShared(shared);
523
524 if (!file.isUpToDate()) {
525 Object[] l = miProviders.toArray();
526 if (l != null) {
527 for (int i = l.length - 1; i >= 0; i--) {
528 synchronized (file) {
529 ((MetaInfoProvider)l[i]).handle(file);
530 }
531 }
532 }
533 file.setLastUpdate(System.currentTimeMillis());
534 }
535 }
536
537 /***
538 * <p>Only invoked from {@link #update()}.
539 */
540 private void removeFile(MetaInfoFile file)
541 {
542 int index = indexOf(file);
543 logger.debug("removed (" + index + ") " + file);
544
545
546 files.set(index, null);
547 indexByFile.remove(file);
548
549 if (file.isShared()) {
550 sharedCount--;
551 }
552
553
554
555 totalCount--;
556
557 if (totalCount % 16 == 0) {
558 updateStatus();
559 }
560
561 listeners.fireItemRemoved(file);
562 }
563
564
565
566 private class UpdateRunner implements Runnable
567 {
568 boolean read;
569
570 public UpdateRunner(boolean read)
571 {
572 this.read = read;
573 }
574
575 public void run()
576 {
577 updateStatus();
578
579 if (read) {
580 read();
581 isRead = true;
582 }
583
584 Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
585
586 while (updatePending) {
587 updatePending = false;
588 update();
589 }
590
591 synchronized (updateLock) {
592 updateRunning = false;
593 }
594
595 updateStatus();
596 }
597 }
598
599 }