/*
* 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;
import static net.frontlinesms.FrontlineSMSConstants.COMMON_UNDEFINED;
import static net.frontlinesms.FrontlineSMSConstants.DEFAULT_END_DATE;
import java.awt.Desktop;
import java.awt.Image;
import java.awt.Toolkit;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import net.frontlinesms.data.domain.*;
import net.frontlinesms.email.EmailException;
import net.frontlinesms.email.smtp.SmtpEmailSender;
import net.frontlinesms.encoding.Base64Utils;
import net.frontlinesms.resources.ResourceUtils;
import net.frontlinesms.ui.i18n.InternationalisationUtils;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
/**
* Class containing general helper methods that have nowhere better to live.
* @author Alex Anderson alex@frontlinesms.com
*/
public class FrontlineUtils {
//> CONSTANTS
/** Logging object */
private static Logger LOG = FrontlineUtils.getLogger(FrontlineUtils.class);
/** Date formatter used in logs. */
private static final SimpleDateFormat LOG_DATE_FORMATTER = new SimpleDateFormat();
static {
loadLogConfiguration();
}
/**
* Reloads the log configuration.
*/
static void loadLogConfiguration() {
File f = new File(ResourceUtils.getConfigDirectoryPath() + ResourceUtils.PROPERTIES_DIRECTORY_NAME + File.separatorChar + "log4j.properties");
if (f.exists()) {
PropertyConfigurator.configure(f.getAbsolutePath());
} else {
PropertyConfigurator.configure(FrontlineUtils.class.getResource("/log4j.properties"));
}
}
/**
* Gets the logging object for a {@link Class}, making sure the expected configuration is used.
* Using this method rather than {@link Logger#getLogger(Class)} directly ensures that {@link #loadLogConfiguration()} has been run.
* TODO This probably isn't the best way of ensuring the log config is loaded. It seems to work okay though.
* @param clazz
* @return logging object for the supplied class
*/
public static Logger getLogger(Class<? extends Object> clazz) {
return Logger.getLogger(clazz);
}
/**
* Gets the date passed in arguments as a long format
* @param dateFieldString The string basically input in a text field
* @param isStartDate A boolean saying whether we're looking for a start or end date
* @return
* @throws ParseException
*/
public static long getLongDateFromStringDate (String dateFieldString, boolean isStartDate) throws ParseException {
if (dateFieldString.length() == 0
|| (!isStartDate && dateFieldString.equals(InternationalisationUtils.getI18nString(COMMON_UNDEFINED)))) {
return (isStartDate ? System.currentTimeMillis() : DEFAULT_END_DATE); // FIXME Should we take the TimeZone into account?
} else {
Date ds = InternationalisationUtils.parseDate(dateFieldString);
Calendar c = Calendar.getInstance();
c.setTime(ds);
// If we are looking for a conversion of a start date, then no worries, because the first milliseconds of
// the given day is returned
if (!isStartDate && !dateFieldString.contains(" ")) { // We just do that if the format is a simple format (without time)
// Otherwise, we're looking for the last milliseconds of the given day
// So, we seek the first millisecond of the day after
c.add(Calendar.DATE, 1);
// And then substitute one millisecond
c.setTimeInMillis(c.getTimeInMillis() - 1);
}
return c.getTime().getTime();
}
}
/**
* Converts a device manufacturer and model into a human-readable string, with extraneous information removed.
* @param manufacturer
* @param model
* @return string containing manufacturer and model information
*/
public static String getManufacturerAndModel(String manufacturer, String model) {
if (model.startsWith(manufacturer)) model = model.substring(model.indexOf(manufacturer) + manufacturer.length()).trim();
return manufacturer + ' ' + model;
}
/**
* Make the current thread sleep; ignore InterruptedExceptions.
* @param millis number of milliseconds to sleep the thread for.
*/
public static void sleep_ignoreInterrupts(long millis) {
try {
Thread.sleep(millis);
} catch(InterruptedException ex) {
LOG.debug("", ex);
}
}
/**
* Returns a string with all contact groups.
* @param contact
* @param groups_delimiter
* @return the groups this contact is a member of represented as a string with vales separated by the requested delimiter
*/
public static String contactGroupsAsString(Collection<Group> groupCollection, String groups_delimiter) {
String groups = "";
for (Group g : groupCollection) {
groups += g.getPath() + groups_delimiter;
}
if (groups.endsWith(groups_delimiter)) {
groups = groups.substring(0, groups.length() - groups_delimiter.length());
}
return groups;
}
/**
* This method makes a http request and returns the response according to the supplied parameter.
* @param url URL to connect.
* @param waitForResponse <code>true</code> if this method should block and return the http response body; <code>false</code> otherwise
* @return the body of the http response, or empty string if the response is not requested
* @throws IOException
*/
public static String makeHttpRequest(String url, boolean waitForResponse) throws IOException {
LOG.trace("ENTER");
String str = "";
URL hp = new URL(url);
HttpURLConnection conn = (HttpURLConnection) hp.openConnection();
int rc = conn.getResponseCode();
LOG.debug("RC = " + rc);
if (rc == HttpURLConnection.HTTP_OK) {
InputStream input = null;
try {
input = conn.getInputStream();
LOG.debug("Wait for response [" + waitForResponse + "]");
if (waitForResponse) {
// Don't check the MIME type here - we don't want to confuse anybody
// Get response data.
BufferedReader inputData = new BufferedReader(new InputStreamReader(input));
StringBuilder sb = new StringBuilder();
while (null != (str = inputData.readLine())) {
sb.append(str + "\n");
}
str = sb.toString();
}
} finally {
if(input != null) try { input.close(); } catch(IOException ex) { LOG.warn("Exception closing HTTP input stream.", ex); }
}
}
LOG.trace("EXIT");
return str;
}
/**
* This method makes a http request and returns the input stream.
* @param url URL to connect.
* @return
* @throws IOException
*/
public static InputStream makeHttpRequest(String url) throws IOException {
LOG.trace("ENTER");
URL hp = new URL(url);
URLConnection conn = hp.openConnection();
conn.connect();
LOG.trace("EXIT");
return conn.getInputStream();
}
/**
* This method executes a external command and returns the input stream.
* @param cmd Command to be executed.
* @return
* @throws IOException
* @throws InterruptedException
*/
public static InputStream executeExternalProgram(String cmd) throws IOException, InterruptedException {
LOG.trace("ENTER");
Process p = Runtime.getRuntime().exec(cmd);
p.waitFor();
LOG.trace("EXIT");
return p.getInputStream();
}
/**
* This method executes a external command and returns the response according to the supplied parameter.
* @param cmd Command to be executed.
* @param waitForResponse <code>true</code> if the command's response should be returned. If <code>true</code>, this method blocks.
* @return empty string if waitForResponse is <code>false</code> or the response was an error, or the standard output text of the command if waitForResponse was <code>true</code>
* @throws IOException
* @throws InterruptedException
*/
public static String executeExternalProgram(String cmd, boolean waitForResponse) throws IOException, InterruptedException {
LOG.trace("ENTER");
String str = "";
Process p = Runtime.getRuntime().exec(cmd);
LOG.debug("Wait for response [" + waitForResponse + "]");
if (waitForResponse) {
int exit = p.waitFor();
LOG.debug("Process exit value [" + exit + "]");
if (exit == 0) {
InputStream inputStream = null;
try {
inputStream = p.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
while (null != ((str = br.readLine()))) {
sb.append(str + "\n");
}
str = sb.toString();
} finally {
if(inputStream != null) try { inputStream.close(); } catch(IOException ex) { LOG.warn("Error closing external program input stream.", ex); }
}
}
}
LOG.trace("EXIT");
return str;
}
/**
* Encodes the supplied string into Base64.
* @param password the string to encode
* @return base64-encoded string
*/
public static String encodeBase64(String password) {
try {
return Base64Utils.encode(password.getBytes("UTF-8"));
} catch (UnsupportedEncodingException ex) {
return Base64Utils.encode(password.getBytes());
}
}
/**
* Decodes the supplied string from Base64.
* @param passwordEncrypted
* @return decoded value of the supplied base64 string
*/
public static String decodeBase64(String passwordEncrypted) {
byte[] data = Base64Utils.decode(passwordEncrypted);
String decoded;
try {
decoded = new String(data, "UTF-8");
} catch(UnsupportedEncodingException ex) {
return new String(data);
}
return decoded;
}
/**
* This class compares files and directories, giving higher priority to directories.
*
* @author Carlos Eduardo Genz
*/
public static class FileComparator implements Comparator<File> {
/**
* compares files and directories, giving higher priority to directories.
*/
public int compare(File arg0, File arg1) {
if (arg0.isDirectory() && arg1.isDirectory()) {
return 0;
} else if (arg0.isDirectory() && !arg1.isDirectory()) {
return -1;
} else return 1;
}
}
/** prioritised guess of users' browser preference */
private static final String[] BROWSERS = {"epiphany", "firefox", "mozilla", "konqueror", "netscape", "opera", "links", "lynx"};
/** Number of milliseconds in a day */
private static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000;
/**
* <p>This method assumes that any URLs starting with something other than http:// are
* links to the FrontlineSMS help manual. On Linux and Windows machines, this is
* assumed to be local. On Mac OSX, we link to a website.</p>
* <p>This code was adapted from http://www.centerkey.com/java/browser/. The original code
* is in the public domain.</p>
* @param url
*/
public static void openExternalBrowser(String url) {
Runtime rt = Runtime.getRuntime();
try {
if (isWindowsOS()) {
LOG.info("Attempting to open URL with Windows-specific code");
String[] cmd = new String[4];
cmd[0] = "cmd.exe";
cmd[1] = "/C";
cmd[2] = "start";
cmd[3] = url;
rt.exec(cmd);
} else if (isMacOS()) {
LOG.info("Attempting to open URL with Mac-specific code");
// TODO here, we are trying to launch the browser twice. This looks to only open
// one browser instance, so I'd guess one of them was failing. If it's the same
// one every time, we can get rid of the other method.
LOG.debug("Trying to open with rt.exec....");
rt.exec("open " + url);
LOG.debug("Trying to open with FileManager...");
Class<?> fileManager = Class.forName("com.apple.eio.FileManager");
Method openURL = fileManager.getDeclaredMethod("openURL", String.class);
openURL.invoke(null, new Object[] {url});
} else {
LOG.info("Attempting to open URL with default code");
StringBuilder cmd = new StringBuilder();
for (int i=0; i < BROWSERS.length; i++)
cmd.append( (i==0 ? "" : " || " ) + BROWSERS[i] +" \"" + url + "\" ");
rt.exec(new String[] { "sh", "-c", cmd.toString() });
}
} catch (Throwable t) {
LOG.warn("Could not open browser (" + url + ")", t);
}
}
/**
* Builds the URL of the web page depending on the OS
* @param page The help page
*/
public static void openHelpPageInBrowser(String page) {
// TODO Try to resolve permission problem for mac and then open local help files
// It seems like we don't have permission to open a file inside a .app package
// on OSX. While we are packaging the mac version as a .app, we access help on
// the FrontlineSMS website.
String url = "help/" + page;
if (!new File(url).exists()) {
if (!page.toLowerCase().startsWith("http")) {
url = getOnlineHelpUrl(page);
} else {
url = page;
}
}
openExternalBrowser(url);
}
/**
* Opens an email editor in the default email client
* @param uri
*/
public static void openDefaultMailClient(URI uri) {
if (uri != null) {
try {
Desktop.getDesktop().mail(uri.resolve(uri.toString().replace("#", "")));
} catch (IOException e) {}
}
}
private final static String getOnlineHelpUrl(String page) {
return "http://help.frontlinesms.com/manuals/" + BuildProperties.getInstance().getVersion() + "/" + page;
}
/**
* Checks if the User's OS is a Mac OS
* @return <code>true</code> if the OS is Mac OS, <code>false</code> otherwise.
*/
public static boolean isMacOS () {
String os = System.getProperty("os.name").toLowerCase();
return os.startsWith("mac");
}
/**
* Checks if the User's OS is a Windows OS
* @return <code>true</code> if the OS is Windows, <code>false</code> otherwise.
*/
public static boolean isWindowsOS () {
String os = System.getProperty("os.name").toLowerCase();
return os.startsWith("win");
}
/**
* Creates an image from the specified resource.
* To speed up loading the same images use a cache (a simple hashtable).
* And flush the resources being used by an image when you won't use it henceforward
*
* @param path is relative or the classpath, or an URL
* @param clazz TODO
* @return the loaded image or null
*/
public static Image getImage(String path, Class<?> clazz) {
if ((path == null) || (path.length() == 0)) {
return null;
}
Image image = null; //(Image) imagepool.get(path);
try {
URL url = clazz.getResource(path); //ClassLoader.getSystemResource(path)
if (url != null) { // contributed by Stefan Matthias Aust
image = Toolkit.getDefaultToolkit().getImage(url);
}
} catch (Throwable e) {}
if (image == null) {
try {
InputStream is = clazz.getResourceAsStream(path);
//InputStream is = ClassLoader.getSystemResourceAsStream(path);
if (is != null) {
byte[] data = new byte[is.available()];
is.read(data, 0, data.length);
image = Toolkit.getDefaultToolkit().createImage(data);
is.close();
}
else { // contributed by Wolf Paulus
image = Toolkit.getDefaultToolkit().getImage(new URL(path));
}
} catch (Throwable e) {}
}
return image;
}
/**
* Formats a date for use in logging - should use default SimpleDateFormat
* style rather than a localised format. For this reason, this should not
* be used for dates that are to be displayed to the user.
* @param date the date to be formatted
* @return date string represetnation of a date, suitably formatted for use in logs
*/
public static String log_formatDate(long date) {
return LOG_DATE_FORMATTER.format(new Date(date));
}
/**
* Calls {@link URLEncoder#encode(String, String)} using UTF-8 as the encoding. If somehow
* an {@link UnsupportedEncodingException} is thrown, this method will just return the original
* {@link String} supplied. This method will also ignore <code>null</code> inputs, rather than
* throwing a {@link NullPointerException}.
* @param string
* @return url-encoded string
*/
public static String urlEncode(String string) {
if(string == null) return null;
try {
string = URLEncoder.encode(string, "UTF-8");
} catch (UnsupportedEncodingException e) { /* This will never happen - UTF-8 should always be supported by every JVM. */ }
return string;
}
/**
* Calls {@link URLDecoder#decode(String, String)} using UTF-8 as the encoding. If somehow
* an {@link UnsupportedEncodingException} is thrown, this method will just return the original
* {@link String} supplied. This method will also ignore <code>null</code> inputs, rather than
* throwing a {@link NullPointerException}.
* @param string
* @return url-decoded string
*/
public static String urlDecode(String string) {
if(string == null) return null;
try {
string = URLDecoder.decode(string, "UTF-8").trim();
} catch (UnsupportedEncodingException e) { /* This will never happen - UTF-8 should always be supported by every JVM. */ }
return string;
}
/**
* Gets the name of the supplied file without the extension.
* @param file
*/
public static String getFilenameWithoutFinalExtension(File file) {
return getFilenameWithoutFinalExtension(file.getName());
}
public static String getFilenameWithoutFinalExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
if(dotIndex > -1) filename = filename.substring(0, dotIndex);
return filename;
}
/**
* Gets the name of the supplied file without any extension.
* @param file
*/
public static String getFilenameWithoutAnyExtension(File file) {
return getFilenameWithoutAnyExtension(file.getName());
}
public static String getFilenameWithoutAnyExtension(String filename) {
int dotIndex = filename.indexOf('.');
if(dotIndex > -1) filename = filename.substring(0, dotIndex);
return filename;
}
public static String getFinalFileExtension(File file) {
return getFinalFileExtension(file.getName());
}
/** return the extension of a file */
public static String getFinalFileExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
if(dotIndex == -1) return "";
else return filename.substring(dotIndex+1);
}
public static String getWholeFileExtension(File file) {
return getWholeFileExtension(file.getName());
}
/** return the extension of a file */
public static String getWholeFileExtension(String filename) {
int dotIndex = filename.indexOf('.');
if(dotIndex == -1) return "";
else return filename.substring(dotIndex+1);
}
/**
* Send an E-Mail to the FrontlineSMS Support email account.
* TODO smtp sending should be refactored into email.smtp.SmtpMessageSender
* @param fromName
* @param fromEmailAddress
* @param attachment
* @throws MessagingException
*/
public static void sendToFrontlineSupport(String fromName, String fromEmailAddress, String subject, String textContent, String attachment) throws EmailException {
sendEmail(FrontlineSMSConstants.FRONTLINE_SUPPORT_EMAIL, fromName, fromEmailAddress, subject, textContent, attachment);
}
/**
* Send an E-Mail to the given e-mail address.
* TODO smtp sending should be refactored into email.smtp.SmtpMessageSender
* @param fromName
* @param fromEmailAddress
* @param attachment
* @throws MessagingException
*/
public static void sendEmail(String recipientEmailAddress, String fromName, String fromEmailAddress, String subject, String textContent, String attachment) throws EmailException {
SmtpEmailSender emailSender = new SmtpEmailSender(FrontlineSMSConstants.FRONTLINE_SUPPORT_EMAIL_SERVER);
emailSender.sendEmail(recipientEmailAddress,
emailSender.getLocalEmailAddress(fromEmailAddress, fromName),
subject,
textContent,
new File(attachment));
}
/**
* Used to calculate the exact last moment a date should
* @param date
* @return
*/
public static Long getFirstMillisecondOfNextDay(Date date) {
return date.getTime() + MILLIS_PER_DAY;
}
public static boolean isSimpleFormat(Date date) {
Calendar cal = new GregorianCalendar();
cal.setTime(date);
return cal.get(Calendar.HOUR_OF_DAY) == 0
&& cal.get(Calendar.MINUTE) == 0
&& cal.get(Calendar.MILLISECOND) == 0;
}
}