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