| CWProject.java |
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