/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
* subject to license terms.
*/
package org.mypsycho.swing.app.session;
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.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.util.logging.Level;
import javax.jnlp.BasicService;
import javax.jnlp.FileContents;
import javax.jnlp.PersistenceService;
import javax.jnlp.ServiceManager;
import javax.jnlp.UnavailableServiceException;
import org.mypsycho.swing.app.ApplicationContext;
import org.mypsycho.swing.app.SwingBean;
import org.mypsycho.swing.app.os.Plateform.PlateformHook;
/**
* 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 SwingBean {
private final ApplicationContext context;
private long storageLimit = -1L;
private LocalIO localIO = null;
private final File unspecifiedFile = new File("unspecified");
private File directory = unspecifiedFile;
public LocalStorage(ApplicationContext context) {
if (context == null) {
throw new IllegalArgumentException("null context");
}
this.context = context;
}
private void log(Level level, String message, Throwable cause) {
context.getApplication().exceptionThrown(level, "localStorage", message, cause);
}
// FIXME - documentation
public final ApplicationContext getContext() {
return context;
}
private void checkFileName(String fileName) {
if (fileName == null) {
throw new IllegalArgumentException("null fileName");
}
}
/**
* Opens an input stream to read from the entry
* specified by the {@code name} parameter.
* If the named entry cannot be opened for reading
* then a {@code IOException} is thrown.
*
* @param fileName the storage-dependent name
* @return an {@code InputStream} object
* @throws IOException if the specified name is invalid,
* or an input stream cannot be opened
*/
public InputStream openInputFile(String fileName) throws IOException {
checkFileName(fileName);
return getLocalIO().openInputFile(fileName);
}
/**
* Opens an output stream to write to the entry
* specified by the {@code name} parameter.
* If the named entry cannot be opened for writing
* then a {@code IOException} is thrown.
* If the named entry does not exist it can be created.
* The entry will be recreated if already exists.
*
* @param fileName the storage-dependent name
* @return an {@code OutputStream} object
* @throws IOException if the specified name is invalid,
* or an output stream cannot be opened
*/
public OutputStream openOutputFile(final String fileName) throws IOException {
return openOutputFile(fileName, false);
}
/**
* Opens an output stream to write to the entry
* specified by the {@code name} parameter.
* If the named entry cannot be opened for writing
* then a {@code IOException} is thrown.
* If the named entry does not exist it can be created.
* You can decide whether data will be appended via append parameter.
*
* @param fileName the storage-dependent name
* @param append if <code>true</code>, then bytes will be written
* to the end of the output entry rather than the beginning
* @return an {@code OutputStream} object
* @throws IOException if the specified name is invalid,
* or an output stream cannot be opened
*/
public OutputStream openOutputFile(String fileName, boolean append) throws IOException {
checkFileName(fileName);
return getLocalIO().openOutputFile(fileName, append);
}
/**
* Deletes the entry specified by the {@code name} parameter.
*
* @param fileName the storage-dependent name
* @throws IOException if the specified name is invalid,
* or an internal entry cannot be deleted
*/
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;
@Override
public void exceptionThrown(Exception e) {
if (exception == null) {
exception = e;
}
}
}
private static boolean persistenceDelegatesInitialized = false;
/**
* Saves the {@code bean} to the local storage
* @param bean the object ot be saved
* @param fileName the targen file name
* @throws IOException
*/
public void save(Object bean, final String fileName) throws IOException {
AbortExceptionListener el = new AbortExceptionListener();
XMLEncoder e = null;
/* Buffer the XMLEncoder's output so that decoding errors don't
* cause us to trash the current version of the specified file.
*/
ByteArrayOutputStream bst = new ByteArrayOutputStream();
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 IOException("save failed \"" + fileName + "\"", el.exception);
}
OutputStream ost = null;
try {
ost = openOutputFile(fileName);
ost.write(bst.toByteArray());
} finally {
if (ost != null) {
ost.close();
}
}
}
/**
* Loads the been from the local storage
* @param fileName name of the file to be read from
* @return loaded object
* @throws IOException
*/
public Object load(String fileName) throws IOException {
InputStream ist;
try {
ist = openInputFile(fileName);
} catch (IOException e) {
return null;
}
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 IOException("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);
// }
// }
// }
/**
* Gets the limit of the local storage
* @return the limit of the local storage
*/
public long getStorageLimit() {
return storageLimit;
}
/**
* Sets the limit of the lical storage
* @param storageLimit the limit of the lical storage
*/
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) {
String id = getContext().getApplication().getProperty(key);
if (id == null) {
log(Level.WARNING, "unspecified resource " + key + " using " + def, null);
id = def;
} else if (id.trim().length() == 0) {
log(Level.WARNING, "empty resource " + key + " using " + def, null);
id = def;
}
return id;
}
private String getApplicationId() {
return getId("Application.id", getContext().getApplication().getClass().getSimpleName());
}
private String getVendorId() {
return getId("Application.vendorId", "UnknownApplicationVendor");
}
/**
* Returns the directory where the local storage is located
* @return the directory where the local storage is located
*/
public File getDirectory() {
if (directory == unspecifiedFile) {
PlateformHook hook = getContext().getPlateform().getHook();
directory = hook.getApplicationHome(getVendorId(), getApplicationId());
}
return directory;
}
/**
* Sets the location of the local storage
* @param directory the location of the local storage
*/
public void setDirectory(File directory) {
File oldValue = this.directory;
this.directory = directory;
firePropertyChange("directory", oldValue, this.directory);
}
/* 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"});
}
@Override
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 {
/**
* Opens an input stream to read from the entry
* specified by the {@code name} parameter.
* If the named entry cannot be opened for reading
* then a {@code IOException} is thrown.
*
* @param fileName the storage-dependent name
* @return an {@code InputStream} object
* @throws IOException if the specified name is invalid,
* or an input stream cannot be opened
*/
public abstract InputStream openInputFile(String fileName) throws IOException;
// /**
// * Opens an output stream to write to the entry
// * specified by the {@code name} parameter.
// * If the named entry cannot be opened for writing
// * then a {@code IOException} is thrown.
// * If the named entry does not exist it can be created.
// * The entry will be recreated if already exists.
// *
// * @param fileName the storage-dependent name
// * @return an {@code OutputStream} object
// * @throws IOException if the specified name is invalid,
// * or an output stream cannot be opened
// */
// public OutputStream openOutputFile(final String fileName) throws IOException {
// return openOutputFile(fileName, false);
// }
/**
* Opens an output stream to write to the entry
* specified by the {@code name} parameter.
* If the named entry cannot be opened for writing
* then a {@code IOException} is thrown.
* If the named entry does not exist it can be created.
* You can decide whether data will be appended via append parameter.
*
* @param fileName the storage-dependent name
* @param append if <code>true</code>, then bytes will be written
* to the end of the output entry rather than the beginning
* @return an {@code OutputStream} object
* @throws IOException if the specified name is invalid,
* or an output stream cannot be opened
*/
public abstract OutputStream openOutputFile(final String fileName, boolean append) throws IOException;
/**
* Deletes the entry specified by the {@code name} parameter.
*
* @param fileName the storage-dependent name
* @throws IOException if the specified name is invalid,
* or an internal entry cannot be deleted
*/
public abstract boolean deleteFile(String fileName) throws IOException;
}
private final class LocalFileIO extends LocalIO {
@Override
public InputStream openInputFile(String fileName) throws IOException {
File path = getFile(fileName);
try {
return new BufferedInputStream(new FileInputStream(path));
} catch (IOException e) {
throw new IOException("couldn't open input file \"" + fileName + "\"", e);
}
}
@Override
public OutputStream openOutputFile(String name, boolean append) throws IOException {
try {
File file = getFile(name);
File dir = file.getParentFile();
if (!dir.isDirectory() && !dir.mkdirs()) {
throw new IOException("couldn't create directory " + dir);
}
return new BufferedOutputStream(new FileOutputStream(file, append));
}
catch (SecurityException exception) {
throw new IOException("could not write to entry: " + name, exception);
}
}
@Override
public boolean deleteFile(String fileName) throws IOException {
File path = new File(getDirectory(), fileName);
return path.delete();
}
private File getFile(String name) throws IOException {
if (name == null) {
throw new IOException("name is not set");
}
return new File(getDirectory(), name);
}
}
/* 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 final 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) {
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 {
if (name == null) {
throw new IOException("name is not set");
}
try {
return new URL(bs.getCodeBase(), name);
} catch (MalformedURLException e) {
throw new IOException("invalid filename \"" + name + "\"", e);
}
}
@Override
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 IOException("openInputFile \"" + fileName + "\" failed", e);
}
}
@Override
public OutputStream openOutputFile(String fileName, boolean append) 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(!append));
} else {
throw new IOException("unable to create FileContents object");
}
} catch (Exception e) {
throw new IOException("openOutputFile \"" + fileName + "\" failed", e);
}
}
@Override
public boolean deleteFile(String fileName) throws IOException {
checkBasics("deleteFile");
URL fileURL = fileNameToURL(fileName);
try {
ps.delete(fileURL);
return true;
} catch (Exception e) {
throw new IOException("openInputFile \"" + fileName + "\" failed", e);
}
}
}
}