View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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      //--- Constant(s) ---
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      // --- Data Field(s) ---
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      // --- Constructor(s) ---
95      
96      public Library()
97      {
98  		prefs.addPropertyChangeListener("libraryDirs", this);
99  		prefs.addPropertyChangeListener("uploadDirs", this);
100 
101 		updateLater(true);
102     }
103 
104     // --- Method(s) ---
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 		// mark all files for deletion
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 		// add library dirs that are not shared
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 		// add shared files (updates shared flag if already in library)
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 			// update
485 			updateFile((MetaInfoFile)file, shared);
486 			
487 			// add new file
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 			// update
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 		// don't acctually remove file to not mess up indicies
546 		files.set(index, null);
547 		indexByFile.remove(file);
548 
549 		if (file.isShared()) {
550 			sharedCount--;
551 		}
552 
553 		// FIX: remove file from search tree here
554 
555 		totalCount--;
556 
557 		if (totalCount % 16 == 0) {
558 			updateStatus();
559 		}
560 
561 		listeners.fireItemRemoved(file);
562     }
563 
564 	//--- Inner Class(es) ---
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 }