Wednesday, March 30, 2011

Automatically delete Tomcat access log old files

On the post Enable Tomcat acess log we saw how Tomcat can be easily configured to log all HTTP requests coming to our server.
For some reason, Tomcat doesn't have a mechanism that also makes sure to delete old access log files. This is a bit weird, because I assume that Tomcat is using Log4j in order to log the requests, and Log4j has a built in mechanism for rotating and deleting old log files.
Anyway, since Tomcat doesn’t take care of all these access log files that are being added, and since a typical production server may produce access log files of several giga per day, your disk might get out of space pretty quick.
we will build a small class that will handle old access log files deletion.
First, we need to get the following information:
  • Tomcat access log files directory.
  • Access log file structure. The default structure is: localhost_access_log.yyyy-MM-dd.txt
  • Access log date format: The default structure is: yyyy-MM-dd
  • Number of access log files we would like to keep as backup. For example, if we choose: 10 it means that we would like to keep the last 10 access log files.
The idea is very simply:
  • We get all access log files on the directory.
  • We extract the date of each log file.
  • We sort all the files on ascending order.
  • We delete the oldest files making sure to keep the requested number of backups.
Let’s have a look at our access log files deletion class. It’s code is quite simple:
package com.bashan.blog.log;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.FilenameFilter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author Bashan
*/
public class TomcatAccessLogCleaner extends TimerTask {
  private static final Log log = LogFactory.getLog(TomcatAccessLogCleaner.class);
  private static final String DEFAULT_LOG_FILE_PATTERN = "localhost_access_log\\.yyyy-MM-dd\\.txt";
  private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
  private String dateFormat;
  private Pattern logFilePat;
  private File logFileDir;
  private int numBackups;
 
  public TomcatAccessLogCleaner(File logFileDir, int numBackups) {
    this(logFileDir, DEFAULT_LOG_FILE_PATTERN, DEFAULT_DATE_FORMAT, numBackups);
}
 
  public TomcatAccessLogCleaner(File logFileDir, String logFilePattern, String dateFormat, int numBackups) {
    this.dateFormat = dateFormat;
    this.logFileDir = logFileDir;
String pat = logFilePattern.replace(dateFormat, "(.+?)");
logFilePat = Pattern.compile(pat);
    this.numBackups = numBackups;
}
 
  public void clean() {
log.info("Starting to clean old Tomcat access logs. Number of backups to keep: " + numBackups);
File[] files = logFileDir.listFiles(new FilenameFilter() {
      public boolean accept(File dir, String file) {
        return logFilePat.matcher(file).matches();
}
});
List<LogFile> logFiles = new ArrayList<LogFile>(files.length);
    for (File file : files) {
      try {
LogFile logFile = new LogFile(file, logFilePat, dateFormat);
logFiles.add(logFile);
}  
      catch (ParseException pe) {
}
 
Collections.sort(logFiles, new Comparator<LogFile>() { 
@Override
      public int compare(LogFile o1, LogFile o2) {
        return o1.getLogDate().compareTo(o2.getLogDate());
}
});
 
    int numFilesToClean = logFiles.size() - numBackups;
    int removed = 0;
    for (int i = 0; i < numFilesToClean; i++) {
LogFile logFile = logFiles.get(i);
log.debug("Deleting access log file: " + logFile);
      if (!logFile.getFile().delete()) {
log.warn("Failed deleting log file");
}
}
log.info("Finished cleaning old Tomcat access logs. Total log files: " +
logFiles.size() + ". Deleted: " + removed + " of " + Math.max(0, numFilesToClean));
}
 
public static class LogFile {
  private File file;
  private Date logDate;
  public LogFile(File file, Pattern pattern, String dateFormat) throws ParseException {
Matcher matcher = pattern.matcher(file.getName());
    if (matcher.find()) {
String dateStr = matcher.group(1);
SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
logDate = sdf.parse(dateStr);
      this.file = file;
}
}
 
  public File getFile() {
    return file;
}
 
  public void setFile(File file) {
    this.file = file;
 
  public Date getLogDate() {
    return logDate;
}
 
  public void setLogDate(Date logDate) {
    this.logDate = logDate;
}
 
  public void run() {
clean();
}
}
Note that TomcatAccessLogCleaner extends TimeTask. This enables easily using this class with a timer, allowing it to run every fix interval, and clean Tomcat access log files.
You can download this class here.
Let’s have a look of an example, showing how this class can be used in a servlet. The class will be scheduled to run automatically every 24 hours and clean old access log files. The class will be scheduled once when servlet loads. Don’t forget that you have to map servlets on your web.xml:
package com.bashan.blog.log;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import java.io.File;
import java.util.Timer;
/**
* @author Bashan
*/
public class TomcatAccessLogCleanerServlet extends HttpServlet {
  public void init(ServletConfig config) throws ServletException {
    super.init(config);
Timer time = new Timer();
TomcatAccessLogCleaner tomcatAccessLogCleaner = new TomcatAccessLogCleaner(new File("c:\\tomcat access log dir"), 10);
time.scheduleAtFixedRate(tomcatAccessLogCleaner, 0, 1000 * 60 * 60 * 24);
}
}
You can download the servlet here.

Monday, March 28, 2011

List of Mobile Devices and Brands as SQL inserts

I compiled a list of all mobile vendor (brands like Apple, Nokia) and models (mobile devices like iPhone, Galaxy S) as SQL inserts.
This list is a good place to start, but mobile devices are being added all the time. If you have a web application that needs to show list of mobile devices this list can help you. Note that the table contains both vendor and model, so it is best to show data in two drop downs: one for vendors and one for models. The drop down of the vendors is used as a filter to the drop down of the models.
This is the structure of the devices table (mySQL style):
CREATE TABLE `device` (
`vendor` varchar(100) NOT NULL,
`model` varchar(100) NOT NULL,
PRIMARY KEY (`vendor`,`model`)
) ENGINE=InnoDB
The list of the inserts if very long. You can download it by using this link.

Tuesday, March 22, 2011

How to Jump to an Anchor using JavaScript

Just like links/urls are a great way of moving between pages, anchors are great way of moving between locations on the same page.
Suppose we would like to move to a specific location on a page above some <div> tag. All we have to do is simply put an <a> tag above that div and give it some name. For example:
<a name="example"></a>
<div>
...
...
...
</div>

How do we jump to this specific location (named: example)? By putting an <a> on the place from which we would like to jump to this location. For example, somewhere else on the page we put:

<a href="#example">Press here to show example</a>

Note, that the name of the anchor for which we would like to jump contains the hash sign (#) as a prefix.

Now, suppose we would like to jump to some location by clicking something other than <a> (anchor) tag. For example by clicking a simple button. How can we jump to that location? Since we are not using anchor tag, we can no longer be directed to that location automatically.

We can use JavaScript in order to accomplish exactly the same behavior. Let’s see how it can be easily done with our button and by setting the “window.location.hash” property :

<input type="button" value="Press here to show example" onclick="window.location.hash = 'example';"/>

Note that on the JavaScript code we no longer need to use the hash (#) prefix.

Monday, March 21, 2011

Automatically Add “www” to your site on Tomcat or: Canonical Hostnames

Sometimes we would like that our website domain will always contain “www”.

I can this of two good reasons (beside of making all your site’s urls neat and unified) for adding “www” to your site urls:

  • To avoid cookie issues: Cookies are being stored for domains. www.example.com and example.com are 2 different domains when it comes to cookies. So if you would like to store some data in a cookie without wondering where did it disappear. Unify all the urls of your site to contain “www” and you are always working on the same domain.
  • To avoid security issues: Sometimes we would like to do some JavaScript coding on our site that may require us to be on the same domain (for example: running some JavaScript code from an iFrame on the parent window). Since url with “www” and url without “www” is considered to be a different domain, by making sure all of our urls contain the “www” prefix, we make sure we will not fall in all kind of cross domain security traps.

So, how do we make sure all our urls will always contain the “www” prefix" on Tomcat?

Luckily for us there is a great Java open source project named tuckey that can easily help us to accomplish that task. Tuckey is Url rewrite filter. It rewrites our urls according to set of predefined rules. As you noticed tuckey is doing much more than just adding “www” to our urls, but this is not for the scope of this post. You can learn more about tuckey and download it from this web site: http://www.tuckey.org/.

After you download and install tuckey on your web application (it is well details on the tuckey web site how this thing can be done), you simply have to add this rule to you urlrewrite.xml file:

<rule>
  <name>Canonical Hostnames</name>
  <condition name="host" operator="notequal">^www.mydomain.com</condition>
  <condition name="host" operator="notequal">^$</condition>
  <from>^/(.*)</from>
  <to type="redirect" last="true">http://www.mydomain.com/$1</to>
</rule>

Make sure that this rule is the first rule on your urlrewrite.xml file.

Tuesday, March 15, 2011

iPhone udid Structure

udid is Unique Device Identifier. Every iPhone device has it's own udid.
Other smart phones (Android, Blackberry) also has an id that uniquely identifies the device.
udid value can be easily taken from the class UIDevice by using this simple objective C line:
NSString* udid = [UIDevice currentDevice].uniqueIdentifier;
I am currently working on a mobile installation tracking process. In order to track installations you must compare between the udid of the device when the installed application is first launched.

While conducting tests with udid, I noticed that iPhone udid structure is different when running on X-Code simulator comparing to running on a real device.
Where running on X-Code simulator the value contains the following pattern:

HHHHHHHH-HHHH-HHHH-HHHH-HHHHHHHHHHHH
where "H" stands for Hex number. Note that if there are letters (A-F) they are capital.
For example:
79550EDC-2A86-5BDF-8EE2-219B7FCBD223
When running on a real device the udid value is 40 hex chars. The letters are small. Note that there are no minus ("-") characters used.
For example:

ce43446816952ff4513e1389c5e1d3ac3a856141
I think that the udid is different in the simulator and on a real device because the simulator simply return the udid of the Mac machine and the Mac machine udid string is different than the iPhone device udid.

Getting radio button value from radio group using jQuery

Getting radio button value from a group of radio buttons is an easy task using jQuery. All you have to do is to apply the correct selector. If we have a group of radio buttons named: radioGroup all if we have to do is:
$('input[name=radioGroup]:checked').val();
Let's have a look how it looks on a real HTML page containing 3 radio buttons and a check button. By clicking the “Check Value” button we will open an alert popup showing the selected radio value:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title> TEST </title>
<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
</head>
<body>  
  <input type="radio" id="inpSmall" name="radioGroup" value="small" checked><label for="inpSmall">Small</label><br>
  <input type="radio" id="inpMedium" name="radioGroup" value="medium"><label for="inpMedium">Medium</label><br>
  <input type="radio" id="inpLarge" name="radioGroup" value="large"><label for="inpLarge">Large</label><br><br>
  <input value="Check Value" type="button" onclick="alert('Radio group value: ' + $('input[name=radioGroup]:checked').val());">
</body>
</html>