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