1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 as published by
5    * the Free Software Foundation. This program is distributed in the hope
6    * that it will be useful, but WITHOUT ANY WARRANTY; without even the
7    * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * Copyright: 2005
18   *     The copyright to this program is held by it's authors.
19   *
20   * ID: $Id: SwordBookPath.java 2223 2012-01-26 21:28:02Z dmsmith $
21   */
22  package org.crosswire.jsword.book.sword;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FilenameFilter;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.net.URI;
30  import java.util.ArrayList;
31  import java.util.List;
32  
33  import org.crosswire.common.util.CWProject;
34  import org.crosswire.common.util.Logger;
35  import org.crosswire.common.util.OSType;
36  import org.crosswire.common.util.PropertyMap;
37  import org.crosswire.common.util.StringUtil;
38  import org.crosswire.jsword.book.BookException;
39  import org.crosswire.jsword.book.Books;
40  
41  /**
42   * This represents all of the Sword Books (aka modules).
43   * 
44   * @see gnu.lgpl.License for license details.<br>
45   *      The copyright to this program is held by it's authors.
46   * @author Joe Walker [joe at eireneh dot com]
47   * @author DM Smith [dmsmith555 at yahoo dot com]
48   */
49  public class SwordBookPath {
50      /**
51       * Some basic name initialization
52       */
53      private SwordBookPath() {
54      }
55  
56      /**
57       * Establish additional locations that Sword may hold books.
58       * 
59       * @param theNewDirs
60       *            The new Sword directories
61       * @throws BookException
62       */
63      public static void setAugmentPath(File[] theNewDirs) throws BookException {
64          File[] newDirs = theNewDirs;
65          if (newDirs == null) {
66              return;
67          }
68  
69          SwordBookPath.augmentPath = newDirs.clone();
70  
71          // Now we need to (re)register ourselves
72          Books.installed().registerDriver(SwordBookDriver.instance());
73      }
74  
75      /**
76       * Retrieve the additional locations that Sword may hold Books.
77       * 
78       * @return The new Sword directory
79       */
80      public static File[] getAugmentPath() {
81          return augmentPath.clone();
82      }
83  
84      /**
85       * Obtain a prioritized path of Book locations. This contains the download
86       * dir as the first location, the user's augment path and finally all the
87       * discovered standard locations.
88       * 
89       * @return the array of Book locations.
90       */
91      public static File[] getSwordPath() {
92          ArrayList<File> swordPath = new ArrayList<File>();
93  
94          // The first place to look for Books
95          swordPath.add(getSwordDownloadDir());
96  
97          // Then all the user's augments
98          if (augmentPath != null) {
99              for (int i = 0; i < augmentPath.length; i++) {
100                 File path = augmentPath[i];
101                 if (!swordPath.contains(path)) {
102                     swordPath.add(path);
103                 }
104             }
105         }
106 
107         File[] defaultPath = getDefaultPaths();
108         // Then all the user's bookDirs
109         if (defaultPath != null) {
110             for (int i = 0; i < defaultPath.length; i++) {
111                 File path = defaultPath[i];
112                 if (!swordPath.contains(path)) {
113                     swordPath.add(path);
114                 }
115             }
116         }
117 
118         return swordPath.toArray(new File[swordPath.size()]);
119     }
120 
121     /**
122      * Get a list of books in a given location.
123      * 
124      * @param bookDir
125      *            the directory in which to look
126      * @return the list of books in that location
127      */
128     public static String[] getBookList(File bookDir) {
129         return bookDir.list(new CustomFilenameFilter());
130     }
131 
132     /**
133      * Search all of the "standard" Sword locations for Books. Remember all the
134      * locations.
135      */
136     private static File[] getDefaultPaths() {
137         // If possible migrate the old location to the new one
138         migrateBookDir();
139 
140         List<File> bookDirs = new ArrayList<File>();
141 
142         String home = System.getProperty(PROPERTY_USER_HOME);
143 
144         // Is sword.conf in the current directory?
145         readSwordConf(bookDirs, ".");
146 
147         // mods.d in the current directory?
148         testDefaultPath(bookDirs, ".");
149 
150         // how about in the library, just next door?
151         testDefaultPath(bookDirs, ".." + File.separator + DIR_SWORD_LIBRARY);
152 
153         // if there is a property set for the sword home directory
154         // The Sword project defines SWORD_HOME, but JSword expects this to be
155         // transformed into sword.home.
156         String swordhome = System.getProperty(PROPERTY_SWORD_HOME);
157         if (swordhome != null) {
158             testDefaultPath(bookDirs, swordhome);
159 
160             // how about in the library, just next door?
161             testDefaultPath(bookDirs, swordhome + File.separator + ".." + File.separator + DIR_SWORD_LIBRARY);
162         }
163 
164         if (System.getProperty("os.name").startsWith("Windows")) {
165             testDefaultPath(bookDirs, DIR_WINDOWS_DEFAULT);
166             // how about in the library, just next door?
167             testDefaultPath(bookDirs, DIR_WINDOWS_DEFAULT + File.separator + ".." + File.separator + DIR_SWORD_LIBRARY);
168         }
169 
170         // .sword in the users home directory?
171         readSwordConf(bookDirs, home + File.separator + DIR_SWORD_CONF);
172 
173         // Check for sword.conf in the usual places
174         String[] sysconfigPaths = StringUtil.split(DIR_SWORD_GLOBAL_CONF, ':');
175         for (int i = 0; i < sysconfigPaths.length; i++) {
176             readSwordConf(bookDirs, sysconfigPaths[i]);
177         }
178 
179         URI userDataArea = OSType.getOSType().getUserAreaFolder(DIR_SWORD_CONF, DIR_SWORD_CONF_ALT);
180 
181         // Check look for mods.d in the sword user data area
182         testDefaultPath(bookDirs, new File(userDataArea.getPath()));
183 
184         // JSword used to hold books in ~/.jsword (or its equivalent) but has
185         // code that will
186         // migrate it to ~/.sword (or its equivalent)
187         // If the migration did not work then use the old area
188         testDefaultPath(bookDirs, new File(CWProject.instance().getWritableProjectDir().getPath()));
189 
190         return bookDirs.toArray(new File[bookDirs.size()]);
191     }
192 
193     private static void readSwordConf(List<File> bookDirs, File swordConfDir) {
194         File sysconfig = new File(swordConfDir, SWORD_GLOBAL_CONF);
195         if (sysconfig.canRead()) {
196             InputStream is = null;
197             try {
198                 PropertyMap prop = new PropertyMap();
199                 is = new FileInputStream(sysconfig);
200                 prop.load(is);
201                 String datapath = prop.get(DATA_PATH);
202                 testDefaultPath(bookDirs, datapath);
203                 datapath = prop.get(AUGMENT_PATH);
204                 testDefaultPath(bookDirs, datapath);
205             } catch (IOException ex) {
206                 log.warn("Failed to read system config file", ex);
207             } finally {
208                 if (is != null) {
209                     try {
210                         is.close();
211                     } catch (IOException e) {
212                         log.warn("Failed to close system config file", e);
213                     }
214                 }
215             }
216         }
217     }
218 
219     private static void readSwordConf(List<File> bookDirs, String swordConfDir) {
220         readSwordConf(bookDirs, new File(swordConfDir));
221     }
222 
223     /**
224      * Check to see if the given directory is a Sword mods.d directory and then
225      * add it to the list if it is.
226      * 
227      * @param bookDirs
228      *            The list to add good paths
229      * @param path
230      *            the path to check
231      */
232     private static void testDefaultPath(List<File> bookDirs, File path) {
233         if (path == null) {
234             return;
235         }
236 
237         File mods = new File(path, SwordConstants.DIR_CONF);
238         if (mods.isDirectory() && mods.canRead()) {
239             bookDirs.add(path);
240         }
241     }
242 
243     /**
244      * Check to see if the given directory is a Sword mods.d directory and then
245      * add it to the list if it is.
246      * 
247      * @param bookDirs
248      *            The list to add good paths
249      * @param path
250      *            the path to check
251      */
252     private static void testDefaultPath(List<File> bookDirs, String path) {
253         if (path == null) {
254             return;
255         }
256 
257         testDefaultPath(bookDirs, new File(path));
258     }
259 
260     private static File getDefaultDownloadPath() {
261         File path = null;
262         File[] possiblePaths = getDefaultPaths();
263 
264         if (possiblePaths != null) {
265             for (int i = 0; i < possiblePaths.length; i++) {
266                 File mods = new File(possiblePaths[i], SwordConstants.DIR_CONF);
267                 if (mods.canWrite()) {
268                     path = possiblePaths[i];
269                     break;
270                 }
271             }
272         }
273 
274         // If it is not found on the path then it doesn't exist yet and needs to
275         // be established
276         if (path == null) {
277             URI userDataArea = OSType.getOSType().getUserAreaFolder(DIR_SWORD_CONF, DIR_SWORD_CONF_ALT);
278             path = new File(userDataArea.getPath());
279         }
280 
281         return path;
282     }
283 
284     private static void migrateBookDir() {
285         // Books should be on this path
286         URI userDataArea = OSType.getOSType().getUserAreaFolder(DIR_SWORD_CONF, DIR_SWORD_CONF_ALT);
287 
288         File swordBookPath = new File(userDataArea.getPath());
289 
290         // The "old" Book location might be in one of two locations
291         // It might be ~/.jsword or the new project dir
292         File oldPath = new File(CWProject.instance().getDeprecatedWritableProjectDir().getPath());
293 
294         if (oldPath.isDirectory()) {
295             migrateBookDir(oldPath, swordBookPath);
296             return;
297         }
298 
299         // now trying the new project dir
300         oldPath = new File(CWProject.instance().getWritableProjectDir().getPath());
301 
302         if (oldPath.isDirectory()) {
303             migrateBookDir(oldPath, swordBookPath);
304             return;
305         }
306 
307         // Finally, it might be ~/.sword
308         oldPath = new File(OSType.DEFAULT.getUserAreaFolder(DIR_SWORD_CONF, DIR_SWORD_CONF_ALT).getPath());
309         if (oldPath.isDirectory()) {
310             migrateBookDir(oldPath, swordBookPath);
311         }
312     }
313 
314     private static void migrateBookDir(File oldPath, File newPath) {
315         // move the modules and confs
316         File oldDataDir = new File(oldPath, SwordConstants.DIR_DATA);
317         File newDataDir = new File(newPath, SwordConstants.DIR_DATA);
318         File oldConfDir = new File(oldPath, SwordConstants.DIR_CONF);
319         File newConfDir = new File(newPath, SwordConstants.DIR_CONF);
320 
321         // move the modules
322         if (!migrate(oldDataDir, newDataDir)) {
323             return;
324         }
325 
326         // move the confs
327         if (!migrate(oldConfDir, newConfDir)) {
328             // oops, restore the modules
329             migrate(newDataDir, oldDataDir);
330         }
331     }
332 
333     private static boolean migrate(File oldPath, File newPath) {
334         if (oldPath.equals(newPath) || !oldPath.exists()) {
335             return true;
336         }
337 
338         // make sure the parent exists
339         File parent = newPath.getParentFile();
340         if (!parent.exists() && !parent.mkdirs()) {
341             return false;
342         }
343 
344         return oldPath.renameTo(newPath);
345     }
346 
347     /**
348      * Get the download directory, which is either the one that the user chose
349      * or that JSword picked for the user.
350      * 
351      * @return Returns the download directory.
352      */
353     public static File getSwordDownloadDir() {
354         if (overrideDownloadDir != null) {
355             return overrideDownloadDir;
356         }
357         return defaultDownloadDir;
358     }
359 
360     /**
361      * @return Returns the download directory that the user chose.
362      */
363     public static File getDownloadDir() {
364         return overrideDownloadDir;
365     }
366 
367     /**
368      * @param dlDir
369      *            The download directory that the user specifies.
370      */
371     public static void setDownloadDir(File dlDir) {
372         if (!"".equals(dlDir.getPath())) {
373             overrideDownloadDir = dlDir;
374             log.debug("Setting sword download directory to: " + dlDir);
375         }
376     }
377 
378     /**
379      * Check that the directories in the version directory really represent
380      * versions.
381      */
382     static class CustomFilenameFilter implements FilenameFilter {
383         /*
384          * (non-Javadoc)
385          * 
386          * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String)
387          */
388         public boolean accept(File parent, String name) {
389             return !name.startsWith(PREFIX_GLOBALS) && name.endsWith(SwordConstants.EXTENSION_CONF);
390         }
391     }
392 
393     /**
394      * Default windows installation directory
395      */
396     private static final String DIR_WINDOWS_DEFAULT = "C:\\Program Files\\CrossWire\\The SWORD Project";
397 
398     /**
399      * Library may be a sibling of DIR_WINDOWS_DEFAULT or SWORD_HOME or CWD
400      */
401     private static final String DIR_SWORD_LIBRARY = "library";
402 
403     /**
404      * Users config directory for Sword in Unix
405      */
406     private static final String DIR_SWORD_CONF = ".sword";
407 
408     /**
409      * Users config directory for Sword in Unix
410      */
411     private static final String DIR_SWORD_CONF_ALT = "Sword";
412 
413     /**
414      * Sword global config file
415      */
416     private static final String SWORD_GLOBAL_CONF = "sword.conf";
417 
418     /**
419      * Sword global config file locations
420      */
421     private static final String DIR_SWORD_GLOBAL_CONF = "/etc:/usr/local/etc";
422 
423     /**
424      * Sword global config file's path to where mods can be found
425      */
426     private static final String DATA_PATH = "DataPath";
427 
428     /**
429      * Sword global config file's path to where mods can be found
430      */
431     private static final String AUGMENT_PATH = "AugmentPath";
432 
433     /**
434      * System property for sword home directory
435      */
436     private static final String PROPERTY_SWORD_HOME = "sword.home";
437 
438     /**
439      * Java system property for users home directory
440      */
441     private static final String PROPERTY_USER_HOME = "user.home";
442 
443     /**
444      * File prefix for config file
445      */
446     private static final String PREFIX_GLOBALS = "globals.";
447 
448     /**
449      * The directory URL
450      */
451     private static File[] augmentPath = new File[0];
452 
453     /**
454      * The directory URL
455      */
456     private static File defaultDownloadDir = getDefaultDownloadPath();
457 
458     /**
459      * The directory URL
460      */
461     private static File overrideDownloadDir;
462 
463     /**
464      * The log stream
465      */
466     private static final Logger log = Logger.getLogger(SwordBookPath.class);
467 
468 }
469