/* * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is * subject to license terms. */ package org.jdesktop.application; import java.awt.Rectangle; import java.beans.DefaultPersistenceDelegate; import java.beans.Encoder; import java.beans.ExceptionListener; import java.beans.Expression; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.logging.Level; import java.util.logging.Logger; import javax.jnlp.BasicService; import javax.jnlp.FileContents; import javax.jnlp.PersistenceService; import javax.jnlp.ServiceManager; import javax.jnlp.UnavailableServiceException; import de.mxro.incl.beansserialization.BeansSerializer; /** * Access to per application, per user, local file storage. * * @see ApplicationContext#getLocalStorage * @see SessionStorage * @author Hans Muller (Hans.Muller@Sun.COM) */ public class LocalStorage extends AbstractBean { private static Logger logger = Logger.getLogger(LocalStorage.class.getName()); private final ApplicationContext context; private long storageLimit = -1L; private LocalIO localIO = null; private final File unspecifiedFile = new File("unspecified"); private File directory = unspecifiedFile; protected LocalStorage(ApplicationContext context) { if (context == null) { throw new IllegalArgumentException("null context"); } this.context = context; } // FIXME - documentation protected final ApplicationContext getContext() { return context; } private void checkFileName(String fileName) { if (fileName == null) { throw new IllegalArgumentException("null fileName"); } } public InputStream openInputFile(String fileName) throws IOException { checkFileName(fileName); return getLocalIO().openInputFile(fileName); } public OutputStream openOutputFile(String fileName) throws IOException { checkFileName(fileName); return getLocalIO().openOutputFile(fileName); } public boolean deleteFile(String fileName) throws IOException { checkFileName(fileName); return getLocalIO().deleteFile(fileName); } /* If an exception occurs in the XMLEncoder/Decoder, we want * to throw an IOException. The exceptionThrow listener method * doesn't throw a checked exception so we just set a flag * here and check it when the encode/decode operation finishes */ private static class AbortExceptionListener implements ExceptionListener { public Exception exception = null; public void exceptionThrown(Exception e) { if (exception == null) { exception = e; } } } private static boolean persistenceDelegatesInitialized = false; public void save(Object bean, final String fileName) throws IOException { AbortExceptionListener el = new AbortExceptionListener(); ByteArrayOutputStream bst = new ByteArrayOutputStream(); /*XMLEncoder e = null; try { e = new XMLEncoder(bst); if (!persistenceDelegatesInitialized) { e.setPersistenceDelegate(Rectangle.class, new RectanglePD()); persistenceDelegatesInitialized = true; } e.setExceptionListener(el); e.writeObject(bean); } finally { if (e != null) { e.close(); } } if (el.exception != null) { throw new LSException("save failed \"" + fileName + "\"", el.exception); }*/ BeansSerializer.save(bean, bst); OutputStream ost = null; try { ost = openOutputFile(fileName); ost.write(bst.toByteArray()); } finally { if (ost != null) { ost.close(); } } } public Object load(String fileName) throws IOException { InputStream ist = null; try { ist = openInputFile(fileName); } catch(IOException e) { return null; } return BeansSerializer.load(ist); /*AbortExceptionListener el = new AbortExceptionListener(); XMLDecoder d = null; try { d = new XMLDecoder(ist); d.setExceptionListener(el); Object bean = d.readObject(); if (el.exception != null) { throw new LSException("load failed \"" + fileName + "\"", el.exception); } return bean; } finally { if (d != null) { d.close(); } }*/ } private void closeStream(Closeable st, String fileName) throws IOException { if (st != null) { try { st.close(); } catch (java.io.IOException e) { throw new LSException("close failed \"" + fileName + "\"", e); } } } public long getStorageLimit() { return storageLimit; } public void setStorageLimit(long storageLimit) { if (storageLimit < -1L) { throw new IllegalArgumentException("invalid storageLimit"); } long oldValue = this.storageLimit; this.storageLimit = storageLimit; firePropertyChange("storageLimit", oldValue, this.storageLimit); } private String getId(String key, String def) { ResourceMap appResourceMap = getContext().getResourceMap(); String id = appResourceMap.getString(key); if (id == null) { logger.log(Level.WARNING, "unspecified resource "+key+" using "+def); id = def; } else if (id.trim().length() == 0) { logger.log(Level.WARNING, "empty resource "+key+" using "+def); id = def; } return id; } private String getApplicationId() { return getId("Application.id", getContext().getApplicationClass().getSimpleName()); } private String getVendorId() { return getId("Application.vendorId", "UnknownApplicationVendor"); } /* The following enum and method only exist to distinguish * Windows and OSX for the sake of getDirectory(). */ private enum OSId { WINDOWS, OSX, UNIX } private OSId getOSId() { PrivilegedAction<String> doGetOSName = new PrivilegedAction<String>() { public String run() { return System.getProperty("os.name"); } }; OSId id = OSId.UNIX; String osName = AccessController.doPrivileged(doGetOSName); if (osName != null) { if (osName.toLowerCase().startsWith("mac os x")) { id = OSId.OSX; } else if (osName.contains("Windows")) { id = OSId.WINDOWS; } } return id; } public File getDirectory() { if (directory == unspecifiedFile) { directory = null; String userHome = null; try { userHome = System.getProperty("user.home"); } catch(SecurityException ignore) { } if (userHome != null) { String applicationId = getApplicationId(); OSId osId = getOSId(); if (osId == OSId.WINDOWS) { File appDataDir = null; try { String appDataEV = System.getenv("APPDATA"); if ((appDataEV != null) && (appDataEV.length() > 0)) { appDataDir = new File(appDataEV); } } catch(SecurityException ignore) { } String vendorId = getVendorId(); if ((appDataDir != null) && appDataDir.isDirectory()) { // ${APPDATA}\{vendorId}\${applicationId} String path = vendorId + "\\" + applicationId + "\\"; directory = new File(appDataDir, path); } else { // ${userHome}\Application Data\${vendorId}\${applicationId} String path = "Application Data\\" + vendorId + "\\" + applicationId + "\\"; directory = new File(userHome, path); } } else if (osId == OSId.OSX) { // ${userHome}/Library/Application Support/${applicationId} String path = "Library/Application Support/"+applicationId+"/"; directory = new File(userHome, path); } else { // ${userHome}/.${applicationId}/ String path = "."+applicationId+"/"; directory = new File(userHome, path); } } } return directory; } public void setDirectory(File directory) { File oldValue = this.directory; this.directory = directory; firePropertyChange("directory", oldValue, this.directory); } /* Papers over the fact that the String,Throwable IOException * constructor was only introduced in Java 6. */ private static class LSException extends IOException { public LSException(String s, Throwable e) { super(s); initCause(e); } public LSException(String s) { super(s); } } /* There are some (old) Java classes that aren't proper beans. Rectangle * is one of these. When running within the secure sandbox, writing a * Rectangle with XMLEncoder causes a security exception because * DefaultPersistenceDelegate calls Field.setAccessible(true) to gain * access to private fields. This is a workaround for that problem. * A bug has been filed, see JDK bug ID 4741757 */ private static class RectanglePD extends DefaultPersistenceDelegate { public RectanglePD() { super(new String[]{"x", "y", "width", "height"}); } protected Expression instantiate(Object oldInstance, Encoder out) { Rectangle oldR = (Rectangle)oldInstance; Object[] constructorArgs = new Object[]{ oldR.x, oldR.y, oldR.width, oldR.height }; return new Expression(oldInstance, oldInstance.getClass(), "new", constructorArgs); } } private synchronized LocalIO getLocalIO() { if (localIO == null) { localIO = getPersistenceServiceIO(); if (localIO == null) { localIO = new LocalFileIO(); } } return localIO; } private abstract class LocalIO { public abstract InputStream openInputFile(String fileName) throws IOException; public abstract OutputStream openOutputFile(String fileName) throws IOException; public abstract boolean deleteFile(String fileName) throws IOException; } private class LocalFileIO extends LocalIO { public InputStream openInputFile(String fileName) throws IOException { File path = new File(getDirectory(), fileName); try { return new BufferedInputStream(new FileInputStream(path)); } catch (IOException e) { throw new LSException("couldn't open input file \"" + fileName + "\"", e); } } public OutputStream openOutputFile(String fileName) throws IOException { File dir = getDirectory(); if (!dir.isDirectory()) { if (!dir.mkdirs()) { throw new LSException("couldn't create directory " + dir); } } File path = new File(dir, fileName); try { return new BufferedOutputStream(new FileOutputStream(path)); } catch (IOException e) { throw new LSException("couldn't open output file \"" + fileName + "\"", e); } } public boolean deleteFile(String fileName) throws IOException { File path = new File(getDirectory(), fileName); return path.delete(); } } /* Determine if we're a web started application and the * JNLP PersistenceService is available without forcing * the JNLP API to be class-loaded. We don't want to * require apps that aren't web started to bundle javaws.jar */ private LocalIO getPersistenceServiceIO() { try { Class smClass = Class.forName("javax.jnlp.ServiceManager"); Method getServiceNamesMethod = smClass.getMethod("getServiceNames"); String[] serviceNames = (String[])getServiceNamesMethod.invoke(null); boolean psFound = false; boolean bsFound = false; for(String serviceName : serviceNames) { if (serviceName.equals("javax.jnlp.BasicService")) { bsFound = true; } else if (serviceName.equals("javax.jnlp.PersistenceService")) { psFound = true; } } if (bsFound && psFound) { return new PersistenceServiceIO(); } } catch (Exception ignore) { // either the classes or the services can't be found } return null; } private class PersistenceServiceIO extends LocalIO { private BasicService bs; private PersistenceService ps; private String initFailedMessage(String s) { return getClass().getName() + " initialization failed: " + s; } PersistenceServiceIO() { try { bs = (BasicService)ServiceManager.lookup("javax.jnlp.BasicService"); ps = (PersistenceService)ServiceManager.lookup("javax.jnlp.PersistenceService"); } catch (UnavailableServiceException e) { logger.log(Level.SEVERE, initFailedMessage("ServiceManager.lookup"), e); bs = null; ps = null; } } private void checkBasics(String s) throws IOException { if ((bs == null) || (ps == null)) { throw new IOException(initFailedMessage(s)); } } private URL fileNameToURL(String name) throws IOException { try { return new URL(bs.getCodeBase(), name); } catch (MalformedURLException e) { throw new LSException("invalid filename \"" + name + "\"", e); } } public InputStream openInputFile(String fileName) throws IOException { checkBasics("openInputFile"); URL fileURL = fileNameToURL(fileName); try { return new BufferedInputStream(ps.get(fileURL).getInputStream()); } catch(Exception e) { throw new LSException("openInputFile \"" + fileName + "\" failed", e); } } public OutputStream openOutputFile(String fileName) throws IOException { checkBasics("openOutputFile"); URL fileURL = fileNameToURL(fileName); try { FileContents fc = null; try { fc = ps.get(fileURL); } catch (FileNotFoundException e) { /* Verify that the max size for new PersistenceService * files is >= 100K (2^17) before opening one. */ long maxSizeRequest = 131072L; long maxSize = ps.create(fileURL, maxSizeRequest); if (maxSize >= maxSizeRequest) { fc = ps.get(fileURL); } } if ((fc != null) && (fc.canWrite())) { return new BufferedOutputStream(fc.getOutputStream(true)); } else { throw new IOException("unable to create FileContents object"); } } catch(Exception e) { throw new LSException("openOutputFile \"" + fileName + "\" failed", e); } } public boolean deleteFile(String fileName) throws IOException { checkBasics("deleteFile"); URL fileURL = fileNameToURL(fileName); try { ps.delete(fileURL); return true; } catch(Exception e) { throw new LSException("openInputFile \"" + fileName + "\" failed", e); } } } }