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, 2008 - 2016
18   *
19   */
20  package org.crosswire.common.util;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.net.URI;
25  
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  /**
30   * The Project class looks after the source of project files. These are per user
31   * files and as such have a different location on different operating systems.
32   * These are:<br>
33   * 
34   * <table>
35   * <tr>
36   * <td>Mac OS X</td>
37   * <td>~/Library/Application Support/JSword</td>
38   * </tr>
39   * <tr>
40   * <td>Win NT/2000/XP/ME/9x</td>
41   * <td>~/Application Data/JSword (~ is all over the place, but Java figures it
42   * out)</td>
43   * </tr>
44   * <tr>
45   * <td>Unix and otherwise</td>
46   * <td>~/.jsword</td>
47   * </tr>
48   * </table>
49   * 
50   * <p>
51   * Previously the location was ~/.jsword, which is unfriendly in the Windows and
52   * Mac world. If this location is found on Mac or Windows, it will be moved to
53   * the new location, if different and possible.
54   * </p>
55   * 
56   * <p>
57   * Note: If the Java System property jsword.home is set and it exists and is
58   * writable then it will be used instead of the above location. This is useful
59   * for USB Drives and other portable implementations of JSword. It is
60   * recommended that this name be JSword.
61   * </p>
62   * 
63   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
64   * @author DM Smith
65   */
66  public final class CWProject {
67      /**
68       * Accessor for the resource singleton.
69       * 
70       * @return the singleton
71       */
72      public static CWProject instance() {
73          return instance;
74      }
75  
76      /**
77       * Required for reset of statics during testing
78       */
79      @SuppressWarnings("unused")
80      private static void reset() {
81          instance = new CWProject();
82      }
83  
84      /**
85       * Establish how this project finds it's resources.
86       * 
87       * @param homeProperty
88       *            a property that is used as the home directory name. If this
89       *            property has a value then homeDir and altHomeDir are ignored.
90       *            Defaults to jsword.home.
91       * @param homeDir
92       *            the name of the directory to be used for Unix. Typically this
93       *            is a hidden directory, that is, it begins with a '.'. Defaults
94       *            to .jsword
95       * @param altHomeDir
96       *            the name of the directory to be used for other OSes. This
97       *            should not be a hidden directory. Defaults to JSword.
98       */
99      public static void setHome(String homeProperty, String homeDir, String altHomeDir) {
100         CWProject.homeProperty = homeProperty;
101         CWProject.homeDirectory = homeDir;
102         CWProject.homeAltDirectory = altHomeDir;
103         instance().establishProjectHome();
104     }
105 
106     /**
107      * Sets the name of the front-end. This then informs the read/write directory of the front-end.
108      * @param frontendName
109      */
110     public void setFrontendName(String frontendName) {
111         this.frontendName = frontendName;
112     }
113 
114     /**
115      * @return the writable home for the front-end settings, that can be kept separate from JSword's
116      * configuration, as well from other front-ends.
117      */
118     public URI getWritableFrontendProjectDir() {
119         establishProjectHome();
120         return this.writableFrontEndHome;
121     }
122 
123     /**
124      * @return the readable home for the front-end settings, that can be kept separate from JSword's
125      * configuration, as well from other front-ends.
126      */
127     public URI getReadableFrontendProjectDir() {
128         establishProjectHome();
129         return this.frontendReadHome;
130     }
131 
132 
133     /**
134      * Get the writable user project directory.
135      * 
136      * @return the writable user project directory.
137      */
138     public URI getWritableProjectDir() {
139         establishProjectHome();
140         return writeHome;
141     }
142 
143     /**
144      * Get the locations where project resources can be found.
145      * 
146      * @return an array of URIs which can be used to look up resources.
147      */
148     public URI[] getProjectResourceDirs() {
149         establishProjectHome();
150         return homes.clone();
151     }
152 
153     /**
154      * Get the location where the project directory used to be.
155      * 
156      * @return ~/.jsword
157      */
158     public URI getDeprecatedWritableProjectDir() {
159         return OSType.DEFAULT.getUserAreaFolder(homeDirectory, homeAltDirectory);
160     }
161 
162     /**
163      * Create a the URI for a (potentially non-existent) file to which we can
164      * write. Typically this is used to store user preferences and application
165      * overrides. This method of acquiring files is preferred over
166      * getResourceProperties() as this is writable and can take into account
167      * user preferences. This method makes no promise that the URI returned is
168      * valid. It is totally untested, so reading may well cause errors.
169      * 
170      * @param subject
171      *            The name (minus the .xxx extension)
172      * @param extension
173      *            The extension, prefixed with a '.' See: {@link FileUtil} for a
174      *            list of popular extensions.
175      * @return The resource as a URI
176      */
177     public URI getWritableURI(String subject, String extension) {
178         return NetUtil.lengthenURI(getWritableProjectDir(), subject + extension);
179     }
180 
181     /**
182      * A directory within the project directory.
183      * 
184      * @param subject
185      *            A name for the subdirectory of the Project directory.
186      * @param create whether to create the directory if it does not exist
187      * @return A file: URI pointing at a local writable directory.
188      * @throws IOException 
189      */
190     public URI getWritableProjectSubdir(String subject, boolean create) throws IOException {
191         URI temp = NetUtil.lengthenURI(getWritableProjectDir(), subject);
192 
193         if (create && !NetUtil.isDirectory(temp)) {
194             NetUtil.makeDirectory(temp);
195         }
196 
197         return temp;
198     }
199 
200     /**
201      * Prevent instantiation.
202      */
203     private CWProject() {
204     }
205 
206     /**
207      * Establishes the user's project directory. In a CD installation, the home
208      * directory on the CD will be read-only. This is not sufficient. We also
209      * need a writable home directory. And in looking up resources, the ones in
210      * the writable directory trump those in the readable directory, allowing
211      * the read-only resources to be overridden.
212      * <p>
213      * Here is the lookup order:
214      * <ol>
215      * <li>Check first to see if the jsword.home property is set.</li>
216      * <li>Check for the existence of a platform specific project area and for
217      * the existence of a deprecated project area (~/.jsword on Windows and Mac)
218      * and if it exists and it is possible "upgrade" to the platform specific
219      * project area. Of these "two" only one is the folder to check.</li>
220      * </ol>
221      * In checking these areas, if the one is read-only, add it to the list and
222      * keep going. However, if it is also writable, then use it alone.
223      */
224     private void establishProjectHome() {
225         if (writeHome == null && readHome == null) {
226             // if there is a property set for the jsword home directory
227             String cwHome = System.getProperty(homeProperty);
228             if (cwHome != null) {
229                 URI home = NetUtil.getURI(new File(cwHome));
230                 if (NetUtil.canWrite(home)) {
231                     writeHome = home;
232                 } else if (NetUtil.canRead(home)) {
233                     readHome = home;
234                 }
235                 // otherwise jsword.home is not usable.
236             }
237         }
238 
239         if (writeHome == null) {
240             URI path = OSType.getOSType().getUserAreaFolder(homeDirectory, homeAltDirectory);
241             URI oldPath = getDeprecatedWritableProjectDir();
242             writeHome = migrateUserProjectDir(oldPath, path);
243         }
244 
245         if (homes == null) {
246             if (readHome == null) {
247                 homes = new URI[] {
248                     writeHome
249                 };
250             } else {
251                 homes = new URI[] {
252                         writeHome, readHome
253                 };
254             }
255 
256             // Now that we know the "home" we can set other global notions of home.
257             // TODO(dms): refactor this to CWClassLoader and NetUtil.
258             CWClassLoader.setHome(getProjectResourceDirs());
259 
260             try {
261                 URI uricache = getWritableProjectSubdir(DIR_NETCACHE, true);
262                 File filecache = new File(uricache.getPath());
263                 NetUtil.setURICacheDir(filecache);
264             } catch (IOException ex) {
265                 // This isn't fatal, it just means that NetUtil will try to use $TMP
266                 // in place of a more permanent solution.
267                 LOGGER.warn("Failed to get directory for NetUtil.setURICacheDir()", ex);
268             }
269 
270             //also attempt to create the front-end home
271             try {
272                 if (this.frontendName != null) {
273                     this.writableFrontEndHome = getWritableProjectSubdir(this.frontendName, true);
274                 }
275             } catch (IOException ex) {
276                 LOGGER.warn("Failed to create writable front-end home.", ex);
277             }
278 
279             //attempt to set front-end readable home, if different
280             if (readHome != null && this.frontendName != null) {
281                 this.frontendReadHome = NetUtil.lengthenURI(this.readHome, this.frontendName);
282             }
283         }
284     }
285 
286     /**
287      * Migrates the user's project dir, if necessary and possible.
288      * 
289      * @param oldPath
290      *            the path to the old, deprecated location
291      * @param newPath
292      *            the path to the new location
293      * @return newPath if the migration was possible or not needed.
294      */
295     private URI migrateUserProjectDir(URI oldPath, URI newPath) {
296         if (oldPath.toString().equals(newPath.toString())) {
297             return newPath;
298         }
299 
300         if (NetUtil.isDirectory(oldPath)) {
301             File oldDir = new File(oldPath.getPath());
302             File newDir = new File(newPath.getPath());
303 
304             // renameTo will return false if it could not rename.
305             // This will happen if the directory already exists.
306             // So ensure that the directory does not currently exist.
307             if (!NetUtil.isDirectory(newPath)) {
308                 if (oldDir.renameTo(newDir)) {
309                     return newPath;
310                 }
311                 return oldPath;
312             }
313         }
314         return newPath;
315     }
316 
317     /**
318      * The cache of downloaded files inside the project directory
319      */
320     private static final String DIR_NETCACHE = "netcache";
321 
322     /**
323      * The homes for this application: first is writable, second (if present) is
324      * read-only and specified by the system property jsword.home.
325      */
326     private URI[] homes;
327 
328     /**
329      * The writable home for this application.
330      */
331     private URI writeHome;
332 
333     /**
334      * The name of the front-end application. This allows front-ends to store information
335      * under the jsword directory, separate from other front-ends
336      */
337     private String frontendName;
338 
339     /**
340      * The readable home for this application, specified by the system property
341      * jsword.home. Null, if jsword.home is also writable.
342      */
343     private URI readHome;
344 
345     /**
346      * Front-end home, where the app can write information to it. Could be null if failed to create
347      */
348     private URI writableFrontEndHome;
349 
350     /**
351      * Front-end read home, could be null if not present
352      */
353     private URI frontendReadHome;
354 
355     /**
356      * System property for home directory
357      */
358     private static String homeProperty = "jsword.home";
359 
360     /**
361      * The JSword user settings directory
362      */
363     private static String homeDirectory = ".jsword";
364 
365     /**
366      * The JSword user settings directory for Mac and Windows
367      */
368     private static String homeAltDirectory = "JSword";
369 
370     /**
371      * The filesystem resources
372      */
373     private static CWProject instance = new CWProject();
374 
375     /**
376      * The log stream
377      */
378     private static final Logger LOGGER = LoggerFactory.getLogger(CWProject.class);
379 }
380