package esmska.persistence; import esmska.Context; import java.beans.IntrospectionException; import java.beans.XMLDecoder; import java.beans.XMLEncoder; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.ArrayList; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.FileUtils; import esmska.data.Config; import esmska.data.Contact; import esmska.data.Contacts; import esmska.data.DeprecatedGateway; import esmska.data.History; import esmska.data.Keyring; import esmska.data.Gateways; import esmska.data.Queue; import esmska.data.SMS; import esmska.data.Gateway; import esmska.data.Signature; import esmska.data.Signatures; import esmska.integration.IntegrationAdapter; import esmska.utils.RuntimeUtils; import java.io.FilenameFilter; import java.text.MessageFormat; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.regex.Pattern; import org.apache.commons.lang.Validate; import org.xml.sax.SAXException; /** Load and store settings and data * * @author ripper */ public class PersistenceManager { private static final Logger logger = Logger.getLogger(PersistenceManager.class.getName()); private static PersistenceManager instance; private static final String PROGRAM_DIRNAME = "esmska"; private static final String GATEWAY_DIRNAME = "gateways"; private static final String BACKUP_DIRNAME = "backups"; private static final String CONFIG_FILENAME = "settings.xml"; private static final String GLOBAL_CONFIG_FILENAME = "esmska.conf"; private static final String CONTACTS_FILENAME = "contacts.csv"; private static final String QUEUE_FILENAME = "queue.csv"; private static final String HISTORY_FILENAME = "history.csv"; private static final String KEYRING_FILENAME = "keyring.csv"; private static final String LOCK_FILENAME = "running.lock"; private static final String LOG_FILENAME = "console.log"; private static final String DEPRECATED_GWS_FILENAME = "deprecated.xml"; private static final String GATEWAY_PROPS_FILENAME = "gateways.json"; private static File configDir = new File(System.getProperty("user.home") + File.separator + ".config", PROGRAM_DIRNAME); private static File dataDir = new File(System.getProperty("user.home") + File.separator + ".local" + File.separator + "share", PROGRAM_DIRNAME); private static File globalGatewayDir = new File(GATEWAY_DIRNAME).getAbsoluteFile(); private static File localGatewayDir = new File(dataDir, GATEWAY_DIRNAME); private static File backupDir = new File(configDir, BACKUP_DIRNAME); private static File configFile = new File(configDir, CONFIG_FILENAME); private static File globalConfigFile = new File(GLOBAL_CONFIG_FILENAME); private static File contactsFile = new File(configDir, CONTACTS_FILENAME); private static File queueFile = new File(configDir, QUEUE_FILENAME); private static File historyFile = new File(configDir, HISTORY_FILENAME); private static File keyringFile = new File(configDir, KEYRING_FILENAME); private static File lockFile = new File(configDir, LOCK_FILENAME); private static File logFile = new File(configDir, LOG_FILENAME); private static File deprecatedGWsFile = new File(globalGatewayDir, DEPRECATED_GWS_FILENAME); private static File gatewayPropsFile = new File(configDir, GATEWAY_PROPS_FILENAME); private static boolean customPathSet; /** Creates a new instance of PersistenceManager */ private PersistenceManager() throws IOException { //adjust program dir according to operating system IntegrationAdapter integration = IntegrationAdapter.getInstance(); if (!customPathSet) { String programDir = integration.getProgramDirName(PROGRAM_DIRNAME); File confDir = integration.getConfigDir(configDir); File datDir = integration.getDataDir(dataDir); if (!configDir.equals(confDir)) { setConfigDir(new File(confDir, programDir).getAbsolutePath()); } if (!dataDir.equals(datDir)) { setDataDir(new File(datDir, programDir).getAbsolutePath()); } logFile = integration.getLogFile(logFile); } //adjust gateway dir according to operating system globalGatewayDir = integration.getGatewayDir(globalGatewayDir); deprecatedGWsFile = new File(globalGatewayDir, DEPRECATED_GWS_FILENAME); //create config dir if necessary if (!configDir.exists() && !configDir.mkdirs()) { throw new IOException("Can't create config dir '" + configDir.getAbsolutePath() + "'"); } if (!(canWrite(configDir) && configDir.canExecute())) { throw new IOException("Can't write or execute the config dir '" + configDir.getAbsolutePath() + "'"); } //create data dir if necessary if (!dataDir.exists() && !dataDir.mkdirs()) { throw new IOException("Can't create data dir '" + dataDir.getAbsolutePath() + "'"); } if (!(canWrite(dataDir) && dataDir.canExecute())) { throw new IOException("Can't write or execute the data dir '" + dataDir.getAbsolutePath() + "'"); } //create local gateways dir if (!localGatewayDir.exists() && !localGatewayDir.mkdirs()) { throw new IOException("Can't create local gateway dir '" + localGatewayDir.getAbsolutePath() + "'"); } //create backup dir if (!backupDir.exists() && !backupDir.mkdirs()) { throw new IOException("Can't create backup dir '" + backupDir.getAbsolutePath() + "'"); } } /** Set config directory */ private static void setConfigDir(String path) { if (instance != null) { throw new IllegalStateException("Persistence manager already exists"); } logger.fine("Setting new config dir path: " + path); configDir = new File(path); backupDir = new File(configDir, BACKUP_DIRNAME); configFile = new File(configDir, CONFIG_FILENAME); contactsFile = new File(configDir, CONTACTS_FILENAME); queueFile = new File(configDir, QUEUE_FILENAME); historyFile = new File(configDir, HISTORY_FILENAME); keyringFile = new File(configDir, KEYRING_FILENAME); lockFile = new File(configDir, LOCK_FILENAME); logFile = new File(configDir, LOG_FILENAME); gatewayPropsFile = new File(configDir, GATEWAY_PROPS_FILENAME); } /** Get configuration directory */ public static File getConfigDir() { return configDir; } /** Set data directory */ private static void setDataDir(String path) { if (instance != null) { throw new IllegalStateException("Persistence manager already exists"); } logger.fine("Setting new data dir path: " + path); dataDir = new File(path); localGatewayDir = new File(dataDir, GATEWAY_DIRNAME); } /** Get data directory */ public static File getDataDir() { return dataDir; } /** Set custom directories */ public static void setCustomDirs(String configDir, String dataDir) { setConfigDir(configDir); setDataDir(dataDir); customPathSet = true; } /** Create instance of PersistenceManager. Should be called only for inicialization, after * that the instance is available in the Context. * @throws IOException could not read/write configuration files/directories */ public static void instantiate() throws IOException { if (instance == null) { instance = new PersistenceManager(); Context.persistenceManager = instance; } else { throw new IllegalStateException("PersistanceManager is already instantiated"); } } /** Get file used for logging */ public File getLogFile() { return logFile; } /** Save program configuration */ public void saveConfig() throws IOException { logger.fine("Saving config..."); //store current program version into config Config.getInstance().setVersion(Config.getLatestVersion()); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); XMLEncoder xmlEncoder = new XMLEncoder(new BufferedOutputStream(out)); xmlEncoder.writeObject(Config.getInstance()); xmlEncoder.flush(); out.getChannel().force(false); xmlEncoder.close(); moveFileSafely(temp, configFile); logger.finer("Saved config into file: " + configFile.getAbsolutePath()); } /** Load program configuration */ public void loadConfig() throws Exception { //system-wide config if (globalConfigFile.exists()) { logger.fine("Loading global config..."); try { ImportManager.importGlobalConfig(globalConfigFile); } catch (Exception ex) { //don't stop here, continue to load local config logger.log(Level.WARNING, "Failed to load global configuration: " + globalConfigFile, ex); } } //per-user config if (configFile.exists()) { try { logger.fine("Loading local config..."); XMLDecoder xmlDecoder = new XMLDecoder( new BufferedInputStream(new FileInputStream(configFile))); Config newConfig = (Config) xmlDecoder.readObject(); xmlDecoder.close(); Config.setSharedInstance(newConfig); } catch (Exception ex) { //set default config Config.setSharedInstance(new Config()); throw ex; } } else { //set default config Config.setSharedInstance(new Config()); } } /** Save contacts */ public void saveContacts() throws IOException { logger.fine("Saving contacts..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); ExportManager.exportContacts(Contacts.getInstance().getAll(), out); out.flush(); out.getChannel().force(false); out.close(); moveFileSafely(temp, contactsFile); logger.finer("Saved contacts into file: " + contactsFile.getAbsolutePath()); } /** Load contacts */ public void loadContacts() throws Exception { logger.fine("Loading contacts..."); if (contactsFile.exists()) { ArrayList<Contact> newContacts = ImportManager.importContacts(contactsFile, ContactParser.ContactType.ESMSKA_FILE); ContinuousSaveManager.disableContacts(); Contacts.getInstance().clear(); Contacts.getInstance().addAll(newContacts); ContinuousSaveManager.enableContacts(); } } /** Save sms queue */ public void saveQueue() throws IOException { logger.fine("Saving queue..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); ExportManager.exportQueue(Queue.getInstance().getAll(), out); out.flush(); out.getChannel().force(false); out.close(); moveFileSafely(temp, queueFile); logger.finer("Saved queue into file: " + queueFile.getAbsolutePath()); } /** Load sms queue */ public void loadQueue() throws Exception { logger.fine("Loading queue"); if (queueFile.exists()) { ArrayList<SMS> newQueue = ImportManager.importQueue(queueFile); ContinuousSaveManager.disableQueue(); Queue.getInstance().clear(); Queue.getInstance().addAll(newQueue); ContinuousSaveManager.enableQueue(); } } /** Save sms history */ public void saveHistory() throws IOException { logger.fine("Saving history..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); ExportManager.exportHistory(History.getInstance().getRecords(), out); out.flush(); out.getChannel().force(false); out.close(); moveFileSafely(temp, historyFile); logger.finer("Saved history into file: " + historyFile.getAbsolutePath()); } /** Load sms history */ public void loadHistory() throws Exception { logger.fine("Loading history..."); if (historyFile.exists()) { ArrayList<History.Record> records = ImportManager.importHistory(historyFile); ContinuousSaveManager.disableHistory(); History.getInstance().clearRecords(); History.getInstance().addRecords(records); ContinuousSaveManager.enableHistory(); } } /** Save keyring. */ public void saveKeyring() throws Exception { logger.fine("Saving keyring..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); ExportManager.exportKeyring(Keyring.getInstance(), out); out.flush(); out.getChannel().force(false); out.close(); moveFileSafely(temp, keyringFile); logger.finer("Saved keyring into file: " + keyringFile.getAbsolutePath()); } /** Load keyring. */ public void loadKeyring() throws Exception { logger.fine("Loading keyring..."); if (keyringFile.exists()) { ContinuousSaveManager.disableKeyring(); Keyring.getInstance().clearKeys(); ImportManager.importKeyring(keyringFile); ContinuousSaveManager.enableKeyring(); } } /** Load gateways * @throws IOException When there is problem accessing gateway directory or files * @throws IntrospectionException When current JRE does not support JavaScript execution * @throws SAXException When related XML files are not valid */ public void loadGateways() throws IOException, IntrospectionException, SAXException { logger.fine("Loading gateways..."); ArrayList<Gateway> globalGateways = new ArrayList<Gateway>(); TreeSet<Gateway> localGateways = new TreeSet<Gateway>(); HashSet<DeprecatedGateway> deprecatedGateways = new HashSet<DeprecatedGateway>(); //global gateways if (globalGatewayDir.exists()) { globalGateways = new ArrayList<Gateway>(ImportManager.importGateways(globalGatewayDir, false)); } else { throw new IOException("Could not find gateways directory '" + globalGatewayDir.getAbsolutePath() + "'"); } //local gateways if (localGatewayDir.exists()) { localGateways = ImportManager.importGateways(localGatewayDir, true); } //deprecated gateways if (deprecatedGWsFile.canRead()) { deprecatedGateways = ImportManager.importDeprecatedGateways(deprecatedGWsFile); } else { logger.warning("Could not find deprecated gateways file: '" + deprecatedGWsFile.getAbsolutePath() + "'"); } //filter out deprecated gateways for (DeprecatedGateway deprecated : deprecatedGateways) { for (Iterator<Gateway> it = globalGateways.iterator(); it.hasNext(); ) { Gateway op = it.next(); if (deprecated.getName().equals(op.getName()) && deprecated.getVersion().compareTo(op.getVersion()) >= 0) { logger.log(Level.FINER, "Global gateway {0} is deprecated, skipping.", op.getName()); it.remove(); } } for (Iterator<Gateway> it = localGateways.iterator(); it.hasNext(); ) { Gateway op = it.next(); if (deprecated.getName().equals(op.getName()) && deprecated.getVersion().compareTo(op.getVersion()) >= 0) { //delete deprecated local gateway logger.log(Level.FINER, "Local gateway {0} is deprecated, deleting...", op.getName()); it.remove(); File opFile = null; try { opFile = new File(op.getScript().toURI()); File opIcon = new File(opFile.getAbsolutePath().replaceFirst("\\.gateway$", ".png")); opFile.delete(); FileUtils.deleteQuietly(opIcon); //icon may not be present } catch (Exception ex) { logger.log(Level.WARNING, "Failed to delete deprecated local gateway " + op.getName() + " (" + opFile + ")", ex); } } } } //replace old global versions with new local ones for (Gateway localOp : localGateways) { int index = globalGateways.indexOf(localOp); if (index >= 0) { Gateway globalOp = globalGateways.get(index); if (localOp.getVersion().compareTo(globalOp.getVersion()) > 0) { globalGateways.set(index, localOp); logger.log(Level.FINER, "Local gateway {0} is newer, replacing global one.", localOp.getName()); } else { //delete legacy local gateways logger.log(Level.FINER, "Local gateway {0} is same or older than global one, deleting...", localOp.getName()); File opFile = null; try { opFile = new File(localOp.getScript().toURI()); File opIcon = new File(opFile.getAbsolutePath().replaceFirst("\\.gateway$", ".png")); opFile.delete(); FileUtils.deleteQuietly(opIcon); //icon may not be present } catch (Exception ex) { logger.log(Level.WARNING, "Failed to delete old local gateway " + localOp.getName() + " (" + opFile + ")", ex); } } } else { globalGateways.add(localOp); logger.log(Level.FINER, "Local gateway {0} is additional to global ones, adding to gateway list.", localOp.getName()); } } //load it Gateways.getInstance().clear(); Gateways.getInstance().addAll(globalGateways); Gateways.getInstance().setDeprecatedGateways(deprecatedGateways); } /** Save new gateway to file. New or updated gateway is saved in global gateway * directory (if there are sufficient permissions), otherwise in local gateway * directory. * * @param scriptName name of the gateway/script (without suffix), not null nor empty * @param scriptContents contents of the gateway script file, not null nor empty * @param icon gateway icon, may be null */ public void saveGateway(String scriptName, String scriptContents, byte[] icon) throws IOException { Validate.notEmpty(scriptName); Validate.notEmpty(scriptContents); logger.fine("Saving gateway..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); File iconTemp = null; FileOutputStream iconOut = null; if (icon != null) { iconTemp = createTempFile(); iconOut = new FileOutputStream(iconTemp); } //export the gateway ExportManager.exportGateway(scriptContents, icon, out, iconOut); out.flush(); out.getChannel().force(false); out.close(); if (icon != null) { iconOut.flush(); iconOut.getChannel().force(false); iconOut.close(); } //move script file to correct location File scriptFileGlobal = new File(globalGatewayDir, scriptName + ".gateway"); File scriptFileLocal = new File(localGatewayDir, scriptName + ".gateway"); if (canWrite(globalGatewayDir) && (!scriptFileGlobal.exists() || canWrite(scriptFileGlobal))) { //first try global dir moveFileSafely(temp, scriptFileGlobal); //set readable for everyone scriptFileGlobal.setReadable(true, false); logger.finer("Saved gateway script into file: " + scriptFileGlobal.getAbsolutePath()); } else if (canWrite(localGatewayDir) && (!scriptFileLocal.exists() || canWrite(scriptFileLocal))) { //second try local dir moveFileSafely(temp, scriptFileLocal); logger.finer("Saved gateway script into file: " + scriptFileLocal.getAbsolutePath()); } else { //report error throw new IOException(MessageFormat.format("Could not save gateway " + "{0} to ''{1}'' nor to ''{2}'' - no write permissions?", scriptName, scriptFileGlobal, scriptFileLocal)); } //move icon file to correct location if (icon != null) { File iconFileGlobal = new File(globalGatewayDir, scriptName + ".png"); File iconFileLocal = new File(localGatewayDir, scriptName + ".png"); if (canWrite(globalGatewayDir) && (!iconFileGlobal.exists() || canWrite(iconFileGlobal))) { //first try global dir moveFileSafely(iconTemp, iconFileGlobal); logger.finer("Saved gateway icon into file: " + iconFileGlobal.getAbsolutePath()); } else if (canWrite(localGatewayDir) && (!iconFileLocal.exists() || canWrite(iconFileLocal))) { //second try local dir moveFileSafely(iconTemp, iconFileLocal); logger.finer("Saved gateway icon into file: " + iconFileLocal.getAbsolutePath()); } else { //report error throw new IOException(MessageFormat.format("Could not save gateway icon " + "{0} to '{1}' nor to '{2}' - no write permissions?", scriptName, iconFileGlobal, iconFileLocal)); } } } /** Load gateway properties. */ public void loadGatewayProperties() throws Exception { logger.fine("Loading gateway config..."); if (gatewayPropsFile.exists()) { ImportManager.importGatewayProperties(gatewayPropsFile); } } /** Save gateway properties. */ public void saveGatewayProperties() throws Exception { logger.fine("Saving gateway properties..."); File temp = createTempFile(); FileOutputStream out = new FileOutputStream(temp); ExportManager.exportGatewayProperties(Gateways.getInstance().getAll(), Signatures.getInstance().getAll(), Signature.DEFAULT, out); out.flush(); out.getChannel().force(false); out.close(); moveFileSafely(temp, gatewayPropsFile); logger.log(Level.FINER, "Saved gateway config into file: {0}", gatewayPropsFile.getAbsolutePath()); } /** Checks if this is the first instance of the program. * Manages instances by using an exclusive lock on a file. * @return true if this is the first instance run; false otherwise */ public boolean isFirstInstance() { try { lock(lockFile); lockFile.deleteOnExit(); } catch (Exception ex) { logger.log(Level.INFO, "Program lock could not be obtained", ex); return false; } return true; } /** Try to obtain an exclusive lock on a File. * @throws IOException if lock can't be obtained */ private void lock(File file) throws IOException { Validate.notNull(file); FileOutputStream out = new FileOutputStream(file); FileChannel channel = out.getChannel(); FileLock lock = channel.tryLock(); if (lock == null) { throw new IOException("Could not lock file: " + file.getAbsolutePath()); } } /** Proceed with a backup. Backs up today's configuration (if not backed up * already). Preserves last 7 backups, older ones are deleted. */ public void backupConfigFiles() throws IOException { BackupManager bm = new BackupManager(backupDir); File[] list = new File[] { configFile, contactsFile, historyFile, keyringFile, queueFile, logFile }; boolean backed = bm.backupFiles(Arrays.asList(list), false); if (backed) { //logfile was backed up, delete it so it won't grow indefinitely, //we will start with a fresh one logFile.delete(); //we also have to delete all files starting with the same name, but //having extra suffix [0-9]+, because they are created when multiple //program instances are run //see http://code.google.com/p/esmska/issues/detail?id=195 File parent = logFile.getParentFile(); final String logName = logFile.getName(); File[] oldLogs = parent.listFiles(new FilenameFilter() { private final Pattern pattern = Pattern.compile( "^" + Pattern.quote(logName) + "\\.[0-9]+$"); @Override public boolean accept(File dir, String name) { return pattern.matcher(name).matches(); } }); if (oldLogs != null) { for (File oldLog : oldLogs) { oldLog.delete(); } } } bm.removeOldBackups(7); } /** Moves file from srcFile to destFile safely (using backup of destFile). * If move fails, exception is thrown and attempt to restore destFile from * backup is made. * @param srcFile source file, not null * @param destFile destination file, not null */ private void moveFileSafely(File srcFile, File destFile) throws IOException { Validate.notNull(srcFile); Validate.notNull(destFile); File backup = backupFile(destFile); try { FileUtils.moveFile(srcFile, destFile); } catch (IOException ex) { logger.log(Level.SEVERE, "Moving of " + srcFile.getAbsolutePath() + " to " + destFile.getAbsolutePath() + " failed, trying to restore from backup", ex); FileUtils.deleteQuietly(destFile); //backup may be null if the destination file didn't exist if (backup != null) { FileUtils.moveFile(backup, destFile); } throw ex; } FileUtils.deleteQuietly(backup); } /** Create temp file and return it. */ private File createTempFile() throws IOException { return File.createTempFile("esmska", null); } /** Copies original file to backup file with same filename, but ending with "~". * DELETES original file! * @return newly created backup file, or null if original file doesn't exist */ private File backupFile(File file) throws IOException { if (!file.exists()) { return null; } String backupName = file.getAbsolutePath() + "~"; File backup = new File(backupName); FileUtils.copyFile(file, backup); file.delete(); return backup; } /** Test if it is possible to write to a certain file/directory. * It doesn't have to exist. This method is available because of Java bug * on Windows which does not check permissions in File.canWrite() but only * read-only bit (<a href="http://www.velocityreviews.com/forums/t303199-java-reporting-incorrect-directory-permission-under-windows-xp.html">reference1</a>, * <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4939819">reference2</a>). * @param file File, existing or not existing; not null * @return true if file can be written, false if not */ public static boolean canWrite(File file) { Validate.notNull(file); if (!RuntimeUtils.isWindows()) { //on POSIX systems file.canWrite() works return file.canWrite(); } //file.canWrite() does not work on Windows boolean success = false; try { if (file.exists()) { if (file.isDirectory()) { //try to create file inside directory String name = "writeTest.esmska"; File f = new File(file, name); while (f.exists()) { name = name + ".1"; f = new File(file, name); } f.createNewFile(); success = f.delete(); } else { //try to open file for writing FileOutputStream out = new FileOutputStream(file); out.close(); success = true; } } else { //try to create and delete the file FileUtils.touch(file); success = file.delete(); } } catch (Exception ex) { //be quiet, don't report anything success = false; } return success; } }