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