| 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 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