/* * Copyright (C) 2012, Katy Hilgenberg. * Special acknowledgments to: Knowledge & Data Engineering Group, University of Kassel (http://www.kde.cs.uni-kassel.de). * Contact: sdcf@cs.uni-kassel.de * * This file is part of the SDCFramework (Sensor Data Collection Framework) project. * * The SDCFramework is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The SDCFramework is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the SDCFramework. If not, see <http://www.gnu.org/licenses/>. */ package de.unikassel.android.sdcframework.transmission; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.security.PublicKey; import java.util.List; import java.util.UUID; import java.util.Vector; import java.util.regex.Pattern; import de.unikassel.android.sdcframework.data.ConcreteDeviceInformation; import de.unikassel.android.sdcframework.data.Sample; import de.unikassel.android.sdcframework.data.SampleCollection; import de.unikassel.android.sdcframework.data.independent.BasicSampleCollection; import de.unikassel.android.sdcframework.data.independent.DeviceInformation; import de.unikassel.android.sdcframework.data.independent.GlobalSerializer; import de.unikassel.android.sdcframework.preferences.facade.TransmissionConfiguration; import de.unikassel.android.sdcframework.transmission.facade.UpdatableTransmissionComponent; import de.unikassel.android.sdcframework.util.FileCompressor; import de.unikassel.android.sdcframework.util.FileUtils; import de.unikassel.android.sdcframework.util.JarCompressionStrategy; import de.unikassel.android.sdcframework.util.Logger; import de.unikassel.android.sdcframework.util.RSAFileEncryptionStrategy; import de.unikassel.android.sdcframework.util.ZipCompressionStrategy; import de.unikassel.android.sdcframework.util.facade.ArchiveTypes; import de.unikassel.android.sdcframework.util.facade.Encryption; import de.unikassel.android.sdcframework.util.facade.FileEncryptionStrategy; import de.unikassel.android.sdcframework.util.facade.LogLevel; import android.content.Context; import android.content.res.AssetManager; /** * A file management component for the transmission service. <br/> * <br/> * Does provide the functionality to create a device information XML file as * well as a temporary file directory. In addition it does manage the archive * creation. * * @author Katy Hilgenberg * */ public class FileManager implements UpdatableTransmissionComponent< TransmissionConfiguration > { /** * The name of the temporary sub directory for files */ public final static String TEMP_DIR_NAME = "tmp"; /** * The filename for the archive without extension */ public final static String ARCHIVE_FILE = "sdcarchive"; /** * The temporary file to store serialized samples in */ private final String sampleFile; /** * The file to store permanent device info in */ private final String deviceFile; /** * The file path and base name without extension for archive files */ private final String archiveFileName; /** * The file to store a new created archive in */ private String currentArchive; /** * The file compressor used to create archives */ private final FileCompressor fileCompressor; /** * The file encryption used to create archives */ private FileEncryptionStrategy encryptionStrategy; /** * Constructor * * @param applicationContext * the application context * @param config * the transmission configuration * @param uuid * the unique device identifier created by the service */ public FileManager( Context applicationContext, TransmissionConfiguration config, UUID uuid ) { String filesPath = applicationContext.getFilesDir().getPath() + File.separatorChar; String tmpPath = filesPath + TEMP_DIR_NAME + File.separatorChar; this.sampleFile = tmpPath + BasicSampleCollection.SAMPLE_COLLECTION_FILE; this.deviceFile = filesPath + DeviceInformation.DEVICE_INFO_FILE; this.archiveFileName = tmpPath + ARCHIVE_FILE + "."; this.fileCompressor = new FileCompressor( new ZipCompressionStrategy() ); this.encryptionStrategy = null; createTmpDirectory( tmpPath ); createDeviceInformation( uuid ); updateConfiguration( applicationContext, config ); } /** * Getter for the current sample file name * * @return the current sample file name */ public final String getSampleFile() { return sampleFile; } /** * Getter for the current archive file name * * @return the current archive file name */ public synchronized final String getCurrentArchive() { return currentArchive; } /** * Getter for the device file name * * @return the device file name */ public final String getDeviceFile() { return deviceFile; } /** * Getter for the fileCompressor * * @return the file ompressor */ public final FileCompressor getFileCompressor() { return fileCompressor; } /** * Does create the temporary directory if it does not exist * * @param dir * the absolute temporary directory filename */ private void createTmpDirectory( String dir ) { File tmpDirectory = FileUtils.fileFromPath( dir ); if ( !tmpDirectory.exists() ) { if ( !tmpDirectory.mkdir() ) { Logger.getInstance().error( this, "Failed to create directory for temporary files!" ); } } else { testForExistingArchive( dir ); } } /** * Does create the device information file to be added to the sample * collection archive file ( if it does not exist ) * * @param uuid * the unique device identifier created by the service */ private void createDeviceInformation( UUID uuid ) { DeviceInformation deviceInformation = new ConcreteDeviceInformation( uuid.toString() ); boolean doUpdate = true; if ( FileUtils.fileFromPath( deviceFile ).exists() ) { // compare for device information changes try { String sContent = FileUtils.readTextFileContent( deviceFile ); DeviceInformation deviceInfo = GlobalSerializer.fromXML( DeviceInformation.class, sContent ); doUpdate = !deviceInformation.equals( deviceInfo ); System.out.println( sContent ); } catch ( Exception e ) { Logger.getInstance().error( this, "Failed to open stored device information file!" ); e.printStackTrace(); } } if ( doUpdate ) { try { FileUtils.writeToTextFile( deviceInformation.toXML(), deviceFile ); Logger.getInstance().debug( this, "Updated device information:\n" + deviceInformation.toXML() ); } catch ( Exception e ) { Logger.getInstance().error( this, "Failed to store device information file!" ); e.printStackTrace(); } } } /** * Does create a new archive containing the device description file and the * XML file with the serialized sample collection * * @param samples * the sample collection * @return true if successful, false otherwise */ public synchronized boolean createArchive( SampleCollection samples ) { currentArchive = null; try { List< String > files = new Vector< String >(); // test for file referencing samples SampleCollection samplesWithFiles = new SampleCollection(); for ( Sample sample : samples ) { if ( sample.getRelatedData() != null ) { samplesWithFiles.add( sample ); } } for ( Sample sample : samplesWithFiles ) { // test for file availability File relatedFile = FileUtils.fileFromPath( sample.getRelatedData() ); if ( !relatedFile.exists() || !relatedFile.isFile() ) { // remove sample from the collection for transmission samples.remove( sample ); Logger.getInstance().error( this, "Related file not found: " + relatedFile.getAbsolutePath() + ". Skipping sample!" ); } else { // add file path to the file transfer list files.add( relatedFile.getAbsolutePath() ); // change file reference entry for the sample to the file name without // path String fileName = FileUtils.fileNameFromPath( sample.getRelatedData() ); sample.updateRelatedData( fileName ); } } // create the new archive GlobalSerializer.serializeToFile( samples, FileUtils.fileFromPath( sampleFile ) ); files.add( deviceFile ); files.add( sampleFile ); currentArchive = createArchiveWithFiles( files ); // optionally apply encryption currentArchive = encryptArchive( currentArchive ); } catch ( Exception e ) { Logger.getInstance().error( this, "Failed to create temporary sample file!" ); e.printStackTrace(); // if we fail, -> clean up any temporary archive deleteFile( currentArchive ); currentArchive = null; } finally { // do always clean up sample file deleteFile( sampleFile ); } return currentArchive != null; } /** * Method to encrypt the archive if configured * * @param sArchive * the archive to encrypt * @return the encrypted archive name, or null in case of errors */ private String encryptArchive( String sArchive ) { String tmpArchive = null; // if configured, encrypt archive for transmission if ( encryptionStrategy != null ) { tmpArchive = archiveFileName + encryptionStrategy.getAlgorithmLetterCode(); if ( !encryptionStrategy.encryptFile( FileUtils.fileFromPath( sArchive ), FileUtils.fileFromPath( tmpArchive ) ) ) { Logger.getInstance().error( this, "Failed to encrypt the archive file!" ); deleteFile( tmpArchive ); tmpArchive = null; } // do always delete the unencrypted archive deleteFile( sArchive ); return tmpArchive; } return sArchive; } /** * Method to create an archive from files * * @param files * the files to add * @return the created archive file path */ private String createArchiveWithFiles( List< String > files ) { String sArchive = archiveFileName + fileCompressor.getArchiveExtension(); if ( fileCompressor.compressFiles( files, sArchive ) ) { if ( Logger.getInstance().getLogLevel().equals( LogLevel.DEBUG ) ) { Long sumOriginalSizes = 0L; for ( String fileName : files ) { sumOriginalSizes += FileUtils.fileFromPath( fileName ).length(); } long size = FileUtils.fileFromPath( sArchive ).length(); // calculate compression ratio float ratio = 100.F; ratio -= ( sumOriginalSizes > 0 ? ( size * 100.F / sumOriginalSizes ) : 100.F ); Logger.getInstance().debug( this, String.format( "archive size: %d bytes, ratio %.2f%%", size, ratio ) ); } return sArchive; } // if we fail -> clean up Logger.getInstance().error( this, "Failed to create archive file!" ); deleteFile( sArchive ); return null; } /** * Test method for existing archives * * @return true if an archive exists, false otherwise */ public synchronized boolean hasArchive() { return currentArchive != null; } /** * Does check for an existing archive file * * @param dir * the absolute temporary directory filename */ private void testForExistingArchive( String dir ) { currentArchive = null; File tmpDir = FileUtils.fileFromPath( dir ); if ( tmpDir.exists() && tmpDir.isDirectory() ) { // create a matcher for file name tests Pattern fileMatcher = Pattern.compile( ".*" + ARCHIVE_FILE + ".*" ); for ( File file : tmpDir.listFiles() ) { if ( fileMatcher.matcher( file.getName() ).matches() ) { currentArchive = file.getAbsolutePath(); break; } } } } /** * Does cleanup any temporary files stored * * @param deleteArchive * flag if archive file shall be deleted as well */ public synchronized void doCleanUp( boolean deleteArchive ) { deleteFile( sampleFile ); if ( deleteArchive ) { deleteFile( currentArchive ); currentArchive = null; } } /** * Does cleanup any temporary files stored */ private void deleteFile( String fileName ) { if ( fileName != null && FileUtils.fileFromPath( fileName ).exists() ) { if ( !FileUtils.deleteFile( fileName ) ) { Logger.getInstance().warning( this, "Failed to delete file " + fileName ); } } } /* * (non-Javadoc) * * @see de.unikassel.android.sdcframework.transmission.facade. * UpdatableTransmissionComponent#updateConfiguration(android.content.Context, * de * .unikassel.android.sdcframework.preferences.facade.TransmissionConfiguration * ) */ @Override public synchronized void updateConfiguration( Context context, TransmissionConfiguration config ) { ArchiveTypes currentType = fileCompressor.getStrategy() instanceof JarCompressionStrategy ? ArchiveTypes.jar : ArchiveTypes.zip; ArchiveTypes newType = config.getArchiveType(); if ( !currentType.equals( newType ) ) { switch ( newType ) { case jar: fileCompressor.setStrategy( new JarCompressionStrategy() ); break; case zip: fileCompressor.setStrategy( new ZipCompressionStrategy() ); break; } } if ( config.isEncryptionEnabled() ) { if ( encryptionStrategy == null ) { try { PublicKey pubKey = getPubKeyFromFilesFolder( context ); if ( pubKey == null ) { pubKey = getPubKeyFromAssetFolder( context ); } encryptionStrategy = new RSAFileEncryptionStrategy( pubKey ); } catch ( Exception e ) { Logger.getInstance().error( this, "Failed to create RSA encryption strategy. Reason: " + e.getMessage() ); e.printStackTrace(); } } } else { encryptionStrategy = null; } } /** * Does load the public key file from the private files folder. * * @param context * the context * @return the public key if found, null otherwise * @throws IOException */ protected PublicKey getPubKeyFromFilesFolder( Context context ) { try { String pubKeyFile = context.getFilesDir().getPath() + File.separatorChar + Encryption.PUBLIC_KEY_FILE; InputStream is = new FileInputStream( FileUtils.fileFromPath( pubKeyFile ) ); return Encryption.readPublicKeyFromStream( is ); } catch ( FileNotFoundException fnfe ) {} catch ( Exception e ) { Logger.getInstance().error( this, "Failed to load public key file from files folder. Reason: " + e.getMessage() ); e.printStackTrace(); } return null; } /** * Does load the public key file from the asset folder. * * @param context * the context * @return the public key if found, null otherwise * @throws IOException */ protected PublicKey getPubKeyFromAssetFolder( Context context ) { try { AssetManager assetManager = context.getResources().getAssets(); InputStream is = assetManager.open( Encryption.PUBLIC_KEY_FILE ); return Encryption.readPublicKeyFromStream( is ); } catch ( Exception e ) { Logger.getInstance().error( this, "Failed to load public key file from asset folder. Reason: " + e.getMessage() ); e.printStackTrace(); } return null; } }