/* * FrontlineSMS <http://www.frontlinesms.com> * Copyright 2007, 2008 kiwanja * * This file is part of FrontlineSMS. * * FrontlineSMS 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. * * FrontlineSMS 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 FrontlineSMS. If not, see <http://www.gnu.org/licenses/>. */ package net.frontlinesms.resources; import java.io.*; import java.net.URL; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.zip.*; import net.frontlinesms.FrontlineUtils; import org.apache.log4j.Logger; /** * Utility methods used for loading resources from the file system. * @author Alex */ public class ResourceUtils { //> CONSTANTS /** System property: user.home */ public static final String SYSPROPERTY_USER_HOME = "user.home"; /** Logging object for this class */ private static Logger LOG = FrontlineUtils.getLogger(ResourceUtils.class); /** The size of byte buffers used in this class. */ private static final int BUFFER_SIZE = 2048; /** Name of directory that discarded resources are put in after an upgrade of FrontlineSMS. */ private static final String GRAVEYARD = "old"; /** The location of {@link UserHomeFilePropertySet} files. */ public static final String PROPERTIES_DIRECTORY_NAME = "properties"; /** The filename extension used for {@link UserHomeFilePropertySet} files. */ private static final String PROPERTIES_EXTENSION = ".properties"; /** Name of the FrontlineSMS resource initialisation file. */ private static final String RESOURCE_INI_FILE = "frontlinesms.ini"; /** Property key within # for the location of the resources directory */ private static final String PROPKEY_RESOURCE_PATH = "resources.path"; //> STATIC PROPERTIES /** Location of resources for this instance of FrontlineSMS. This is set via the method {#getConfigDirectoryPath()} the first time we try to access the field. */ private static String resourcePath; //> STATIC UTILITY METHODS /** @return the user home path */ public static String getUserHome() { return System.getProperty("user.home"); } /** * Unzips a compressed archive to the specified output directory. The archive's directory * structure is rebuilt in the output directory if it does not already exist. Optionally, * old versions of files can be kept if they are present. * @param inputArchive * @param outputDirectory * @param overwrite * @throws IOException */ public static final void unzip(File inputArchive, File outputDirectory, boolean overwrite) throws IOException { if(!inputArchive.exists() || !inputArchive.isFile()) throw new IllegalArgumentException("Input archive not found: " + inputArchive.getPath()); if(!outputDirectory.exists() || !outputDirectory.isDirectory()) throw new IllegalArgumentException("Output directory does not exist: " + outputDirectory.getPath()); unzip(new FileInputStream(inputArchive), outputDirectory, overwrite); } /** * Unzips a {@link ZipInputStream} to a directory. If unzipped files already exist in the destination * directory, they can be optionally overridden. * @param inputArchiveAsStream * @param outputDirectory * @param overwriteOverwriteables * @throws IOException */ public static final void unzip(InputStream inputArchiveAsStream, File outputDirectory, boolean overwriteOverwriteables) throws IOException { ZipInputStream in = new ZipInputStream(new BufferedInputStream(inputArchiveAsStream)); byte[] buffer = new byte[BUFFER_SIZE]; ZipEntry entry; String graveyardName = GRAVEYARD + "_" + generateGraveyardTimestamp() + File.separator; while((entry=in.getNextEntry()) != null) { if(!entry.isDirectory()) { boolean remove = false; File outputFile = new File(outputDirectory, entry.getName()); outputFile.getParentFile().mkdirs(); if (outputFile.exists() && overwriteOverwriteables && isOverwriteable(outputFile)) { File graveyard = new File(outputFile.getParentFile(), graveyardName); graveyard.mkdir(); File destination = new File(graveyard, outputFile.getName()); BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(destination), BUFFER_SIZE); ResourceUtils.stream2stream(new FileInputStream(outputFile), out, buffer); out.close(); remove = true; } if (!outputFile.exists() || remove) { BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outputFile), BUFFER_SIZE); ResourceUtils.stream2stream(in, out, buffer); out.close(); } } } in.close(); } /** * Generate a timestamp to be appended to the graveyard directories' names. * @return string timestamp in the form YYYYMMDDHHSSMM */ private static String generateGraveyardTimestamp() { Calendar cal = Calendar.getInstance(); return "" + Integer.toString(10000 + cal.get(Calendar.YEAR)).substring(1) + Integer.toString( 100 + (cal.get(Calendar.MONTH) + 1)).substring(1) + Integer.toString( 100 + cal.get(Calendar.DAY_OF_MONTH)).substring(1) + Integer.toString( 100 + cal.get(Calendar.HOUR_OF_DAY)).substring(1) + Integer.toString( 100 + cal.get(Calendar.MINUTE)).substring(1) + Integer.toString( 1000 + cal.get(Calendar.MILLISECOND)).substring(1); } /** * Checks if a configuration file should be over-ridden by a new version when FrontlineSMS is upgraded. * @param outputFile * @return <code>true</code> if the supplied file should be overwritten; <code>false</code> otherwise */ private static boolean isOverwriteable(File outputFile) { // Overwrite all files, as we dump old files in the graveyard. This should remove any painful // upgrade procedures we might have to go through // TODO this should be made less indiscriminate - it would actually be very useful to keep a lot of config files return true; } /** * Zips the contents of a directory into a new archive. * @param dataDirectoryPath * @param outputArchive * @throws IOException */ public static void zip(String dataDirectoryPath, File outputArchive) throws IOException { File dataDirectory = new File(dataDirectoryPath); LOG.debug("Bundling: " + dataDirectory.getPath()); LOG.debug(" to: " + outputArchive.getPath()); ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outputArchive))); if(!dataDirectory.exists() || !dataDirectory.isDirectory()) throw new IllegalArgumentException("Not a directory: " + dataDirectory.getPath()); byte[] buffer = new byte[ResourceUtils.BUFFER_SIZE]; addDirectoryToZip(out, dataDirectory, dataDirectoryPath, buffer); out.close(); } /** * Recursively adds a directory and its contents to a {@link ZipOutputStream}. * @param out The {@link ZipOutputStream} to zip the directory to * @param directory directory to zip * @param rootDirectory * @param buffer * @throws IOException */ private static final void addDirectoryToZip(ZipOutputStream out, File directory, String rootDirectory, byte[] buffer) throws IOException { LOG.debug("Adding dir to zip: " + directory.getPath()); for(File file : directory.listFiles()) { if(file.isDirectory()) addDirectoryToZip(out, file, rootDirectory, buffer); else addFileToZip(out, file, rootDirectory, buffer); } } /** * Adds a file to a ZipOutputStream. * @param out * @param file * @param rootDirectory base directory being zipped. Necessary here so that the zipped file can be given a relative path * @param buffer * @throws IOException */ private static final void addFileToZip(ZipOutputStream out, File file, String rootDirectory, byte[] buffer) throws IOException { LOG.debug("Adding file to zip: " + file.getPath()); ZipEntry entry = new ZipEntry(file.getPath().substring(rootDirectory.length())); out.putNextEntry(entry); BufferedInputStream in = new BufferedInputStream(new FileInputStream(file), BUFFER_SIZE); ResourceUtils.stream2stream(in, out, buffer); in.close(); } /** * Writes the entire contents of an InputStream to an OutputStream. * @param in * @param out * @param buffer * @throws IOException */ public static final void stream2stream(InputStream in, OutputStream out, byte[] buffer) throws IOException { int bytesRead; while ((bytesRead=in.read(buffer, 0, BUFFER_SIZE)) != -1) { out.write(buffer, 0, bytesRead); } } /** * Loads a list from a textfile, ignoring any blank lines, or lines that * start with a # character. * * FIXME this appears to be charset dependent, which is very naughty * * @param filename * @return array of lines containing useful data from the supplied file */ public static final String[] getUsefulLines(String filename) { FileInputStream fis = null; InputStreamReader isr = null; BufferedReader br = null; ArrayList<String> lines = new ArrayList<String>(); try { fis = new FileInputStream(filename); isr = new InputStreamReader(fis); br = new BufferedReader(isr); String line; while((line = br.readLine()) != null) { line = line.trim(); // Don't forget to ignore empty lines and comments if (line.length() > 0 && line.charAt(0) != '#') { lines.add(line); } } } catch (IOException ex) { LOG.debug("Error reading file '" + filename + "'", ex); } finally { // close any streans, readers etc. if (br != null) try { br.close(); } catch(IOException ex) {} if (isr != null) try { isr.close(); } catch(IOException ex) {} if (fis != null) try { fis.close(); } catch(IOException ex) {} } return lines.toArray(new String[lines.size()]); } public static final List<String> getUsefulLines(URL resourceUrl) { InputStream fis = null; InputStreamReader isr = null; BufferedReader br = null; ArrayList<String> lines = new ArrayList<String>(); try { fis = resourceUrl.openConnection().getInputStream(); isr = new InputStreamReader(fis); br = new BufferedReader(isr); String line; while((line = br.readLine()) != null) { line = line.trim(); // Don't forget to ignore empty lines and comments if (line.length() > 0 && line.charAt(0) != '#') { lines.add(line); } } } catch (IOException ex) { LOG.debug("Error reading url '" + resourceUrl + "'", ex); } finally { // close any streans, readers etc. if (br != null) try { br.close(); } catch(IOException ex) {} if (isr != null) try { isr.close(); } catch(IOException ex) {} if (fis != null) try { fis.close(); } catch(IOException ex) {} } return lines; } /** Gets the directory containing the properties files. */ public static File getPropertiesDirectory() { return new File(getConfigDirectoryPath(), PROPERTIES_DIRECTORY_NAME); } /** * Gets the path to the configuration directory in which languages, conf, and properties directories all lie. * @return path to the directory containing resources for FrontlineSMS */ public synchronized static String getConfigDirectoryPath() { // If we have not checked this before, then we load the magic properties file from the // working directory which dictates where the other properties files are located. if(resourcePath == null) { try { File resourceLocationsFile = new File(RESOURCE_INI_FILE); HashMap<String, String> resourceLocation = FilePropertySet.loadPropertyMap(resourceLocationsFile); resourcePath = resourceLocation.get(PROPKEY_RESOURCE_PATH); } catch(Throwable t) { // If there is a problem loading the path from the working directory, then we just // use the old default that used to be the only option. LOG.warn("Problem locating resource path property.", t); } // If resource path has not been set yet, use the default value if(resourcePath == null) { resourcePath = getUserHome() + File.separatorChar + "FrontlineSMS" + File.separatorChar; } // If the resource path does not end with a /, add one so that we don't have to worry about this later if(resourcePath.charAt(resourcePath.length()-1) != File.separatorChar) { resourcePath += File.separatorChar; } } return resourcePath; } /** * Gets the path of the file where a {@link UserHomeFilePropertySet} is persisted. * @param propertySetName * @return the path to a particular property file */ protected static final File getPropertiesFile(String propertySetName) { return new File(ResourceUtils.getConfigDirectoryPath() + PROPERTIES_DIRECTORY_NAME + File.separatorChar + propertySetName + PROPERTIES_EXTENSION); } }