/* * Copyright (c) 2009, 2012 IBM Corp. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Dave Locke - initial API and implementation and/or initial documentation */ package org.eclipse.paho.client.mqttv3.persist; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.util.Enumeration; import java.util.Vector; import org.eclipse.paho.client.mqttv3.MqttClientPersistence; import org.eclipse.paho.client.mqttv3.MqttPersistable; import org.eclipse.paho.client.mqttv3.MqttPersistenceException; import org.eclipse.paho.client.mqttv3.internal.FileLock; import org.eclipse.paho.client.mqttv3.internal.MqttPersistentData; /** * An implementation of the {@link MqttClientPersistence} interface that provides * file based persistence. * * A directory is specified when the Persistence object is created. When the persistence * is then opened (see {@link #open(String, String)}), a sub-directory is made beneath the base * for this client ID and connection key. This allows one persistence base directory * to be shared by multiple clients. * * The sub-directory's name is created from a concatenation of the client ID and connection key * with any instance of '/', '\\', ':' or ' ' removed. */ public class MqttDefaultFilePersistence implements MqttClientPersistence { private File dataDir; private File clientDir = null; private FileLock fileLock = null; private static final String MESSAGE_FILE_EXTENSION = ".msg"; private static final String MESSAGE_BACKUP_FILE_EXTENSION = ".bup"; private static final String LOCK_FILENAME = ".lck"; private static final FilenameFilter FILE_FILTER = new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(MESSAGE_FILE_EXTENSION); } }; public MqttDefaultFilePersistence() { //throws MqttPersistenceException { this(System.getProperty("user.dir")); } /** * Create an file-based persistent data store within the specified directory. * @param directory the directory to use. */ public MqttDefaultFilePersistence(String directory) { //throws MqttPersistenceException { dataDir = new File(directory); } public void open(String clientId, String theConnection) throws MqttPersistenceException { if (dataDir.exists() && !dataDir.isDirectory()) { throw new MqttPersistenceException(); } else if (!dataDir.exists() ) { if (!dataDir.mkdirs()) { throw new MqttPersistenceException(); } } if (!dataDir.canWrite()) { throw new MqttPersistenceException(); } StringBuffer keyBuffer = new StringBuffer(); for (int i=0;i<clientId.length();i++) { char c = clientId.charAt(i); if (isSafeChar(c)) { keyBuffer.append(c); } } keyBuffer.append("-"); for (int i=0;i<theConnection.length();i++) { char c = theConnection.charAt(i); if (isSafeChar(c)) { keyBuffer.append(c); } } String key = keyBuffer.toString(); clientDir = new File(dataDir,key); if (!clientDir.exists()) { clientDir.mkdir(); } try { fileLock = new FileLock(clientDir,LOCK_FILENAME); } catch (Exception e) { throw new MqttPersistenceException(MqttPersistenceException.REASON_CODE_PERSISTENCE_IN_USE); } // Scan the directory for .backup files. These will // still exist if the JVM exited during addMessage, before // the new message was written to disk and the backup removed. restoreBackups(clientDir); } /** * Checks whether the persistence has been opened. * @throws MqttPersistenceException if the persistence has not been opened. */ private void checkIsOpen() throws MqttPersistenceException { if (clientDir == null) { throw new MqttPersistenceException(); } } public void close() throws MqttPersistenceException { // checkIsOpen(); if (fileLock != null) { fileLock.release(); } if (getFiles().length == 0) { clientDir.delete(); } clientDir = null; } /** * Writes the specified persistent data to the previously specified persistence directory. * This method uses a safe overwrite policy to ensure IO errors do not lose messages. * @param message * @throws MqttPersistenceException */ public void put(String key, MqttPersistable message) throws MqttPersistenceException { checkIsOpen(); File file = new File(clientDir, key+MESSAGE_FILE_EXTENSION); File backupFile = new File(clientDir, key+MESSAGE_FILE_EXTENSION+MESSAGE_BACKUP_FILE_EXTENSION); if (file.exists()) { // Backup the existing file so the overwrite can be rolled-back boolean result = file.renameTo(backupFile); if (!result) { backupFile.delete(); file.renameTo(backupFile); } } try { FileOutputStream fos = new FileOutputStream(file); fos.write(message.getHeaderBytes(), message.getHeaderOffset(), message.getHeaderLength()); if (message.getPayloadBytes()!=null) { fos.write(message.getPayloadBytes(), message.getPayloadOffset(), message.getPayloadLength()); } fos.getFD().sync(); fos.close(); if (backupFile.exists()) { // The write has completed successfully, delete the backup backupFile.delete(); } } catch (IOException ex) { throw new MqttPersistenceException(ex); } finally { if (backupFile.exists()) { // The write has failed - restore the backup boolean result = backupFile.renameTo(file); if (!result) { file.delete(); backupFile.renameTo(file); } } } } public MqttPersistable get(String key) throws MqttPersistenceException { checkIsOpen(); MqttPersistable result; try { File file = new File(clientDir, key+MESSAGE_FILE_EXTENSION); FileInputStream fis = new FileInputStream(file); int size = fis.available(); byte[] data = new byte[size]; int read = 0; while (read<size) { read += fis.read(data,read,size-read); } fis.close(); result = new MqttPersistentData(key, data, 0, data.length, null, 0, 0); } catch(IOException ex) { throw new MqttPersistenceException(ex); } return result; } /** * Deletes the data with the specified key from the previously specified persistence directory. */ public void remove(String key) throws MqttPersistenceException { checkIsOpen(); File file = new File(clientDir, key+MESSAGE_FILE_EXTENSION); if (file.exists()) { file.delete(); } } /** * Returns all of the persistent data from the previously specified persistence directory. * @return all of the persistent data from the persistence directory. * @throws MqttPersistenceException */ public Enumeration keys() throws MqttPersistenceException { checkIsOpen(); File[] files = getFiles(); Vector result = new Vector(files.length); for (int i=0;i<files.length;i++) { String filename = files[i].getName(); String key = filename.substring(0,filename.length()-MESSAGE_FILE_EXTENSION.length()); result.addElement(key); } return result.elements(); } private File[] getFiles() throws MqttPersistenceException { checkIsOpen(); File[] files = clientDir.listFiles(FILE_FILTER); if (files == null) { throw new MqttPersistenceException(); } return files; } private boolean isSafeChar(char c) { return Character.isJavaIdentifierPart(c) || c=='-'; } /** * Identifies any backup files in the specified directory and restores them * to their original file. This will overwrite any existing file of the same * name. This is safe as a stray backup file will only exist if a problem * occured whilst writing to the original file. * @param dir The directory in which to scan and restore backups */ private void restoreBackups(File dir) throws MqttPersistenceException { File[] files = dir.listFiles(new FileFilter() { public boolean accept(File f) { return f.getName().endsWith(MESSAGE_BACKUP_FILE_EXTENSION); } }); if (files == null) { throw new MqttPersistenceException(); } for (int i=0;i<files.length;i++) { File originalFile = new File(dir,files[i].getName().substring(0,files[i].getName().length()-MESSAGE_BACKUP_FILE_EXTENSION.length())); boolean result = files[i].renameTo(originalFile); if (!result) { originalFile.delete(); files[i].renameTo(originalFile); } } } public boolean containsKey(String key) throws MqttPersistenceException { checkIsOpen(); File file = new File(clientDir, key+MESSAGE_FILE_EXTENSION); return file.exists(); } public void clear() throws MqttPersistenceException { checkIsOpen(); File[] files = getFiles(); for (int i=0; i<files.length; i++) { files[i].delete(); } } }