People sometimes accuse me of making my demonstration programs too complex. What I'm trying to show is how even a relatively simple application can take advantage of facilities you might have in place. What's the point of running a database like Oracle if you don't use it to store information? And it only makes sense to use EJBs to retrieve the data since you're using that architectural layer to implement the business logic. Here's a diagram of the layout:

In this example, I go to the database to pull up information for inclusion in a dynamically generated calendar. The nature of the included data isn't as important as the mechanism involved. We could just as easily use this framework to generate appointment information or deadline reminders. Since the formatting is separate from the table generation, I could easily adjust the fonts so as to display a very small calendar anywhere on a web page. And that demonstrates the power of reusability and the component architecture.

I manage the website of a friend down in Atlanta. It was for his site that I originally created the simple calendar application. For the benefit of his customers, I included things like the sunrise/sunset times and average high and low daily temperatures. The time information would have to be assembled at the end of each year and then built into the static HTML pages. Given the resources at my disposal, I decided to automate the process.

Automation in this case involved a lot of manual work! I eventually located a site run by the U.S. Naval Observatory which generates sunrise/sunset tables for a specified location (designated by latitude and longitude) for any year. I needed to encapsulate the logic of connecting to the site, requesting the information, parsing it out and storing it in a database. I wanted to be able to keep the least amount of data in the database, loading only when needed to service a request.

Let me explain that further. Suppose someone is looking at the calendar page for December, 2003 and clicks the button for the following month. When the stateless session EJB tries to load the data it finds none. It can then call the loader routine which will go out to the USNO site and populate the tables with data for all of 2004. Instead of me going out and populating the tables manually, it has become an automated process.

Rather than dealing with proprietary time formats, I decided to store them as straight text. Here's the SQL for the table:

create table calendar (
	location_id	decimal(3,0) NOT NULL,
	cal_date	date NOT NULL,
	cal_sunrise	char(5),
	cal_sunset	char(5),
	primary key ( location_id, cal_date ),
	foreign key ( location_id ) references location
);

The location_id is a foreign key into the location table which turns out to be the perfect place for keeping the latitude and longitude information. Here's that table:

create table location (
	location_id	decimal(3,0) NOT NULL,
	location_name	varchar(50) NOT NULL,
	location_lat	decimal(7,2),
	location_long	decimal(7,2),
	primary key ( location_id )
);

I dug up about 60 years of historical temperature data for Atlanta and averaged it all out. I was able to locate data for more than 160 years for Toronto. This information goes into another database table which is accessed by location identifier, month and day, not a complete date. We don't require a date since the information is constant over the years, hence no need for a year component. Here's the table:

create table temperatures (
	location_id	int,
	temp_month	int,
	temp_day	int,
	temp_high	decimal(6,2),
	temp_low	decimal(6,2),
	primary key ( location_id, temp_month, temp_day ),
	foreign key ( location_id ) references location
);

The actual JSP is a model of simplicity:

<%@ taglib uri="utils" prefix="utils" %>
<head>
<title>Calendar Demo</title>
<style type="text/css">
<!--
th.calendar { font: 12pt Arial; font-weight: bold; color: white; background-color: black }
td.weekday { font: 14pt Arial; text-align: right; vertical-align: top; height: 70; background-color:lightblue }
td.weekend { font: 14pt Arial; text-align: right; vertical-align: top; height: 70; background-color:lightgreen }
th.navigation { font-weight: bold; text-align: center }
td.navigation { font: 14pt Arial; font-weigth: bold; text-align: center }
p.sunrise { font: 11pt Arial; font-weight: normal; color: yellow; display: inline }
p.sunset { font: 11pt Arial; font-weight: normal; color: #FF8800; display: inline }
p.slash { font: 11pt Arial; font-weight: normal; color: white; display: inline }
p.temphigh { font: 11pt Arial; font-weight: normal; color: #FF0000; display: inline }
p.templow { font: 11pt Arial; font-weight: normal; color: aqua; display: inline }
-->
</style>
</head>
<body>
<center>
<p>
<utils:setDate/>
<p>
<form>
<table width="500" border="2">
<utils:showCalendar/>
</table>
<p>
<table cellpadding="7">
<utils:showNavigation/>
</table>
</form>
</center>
</body>

Here's the web.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app id="WebApp_ID">
   <servlet id="Servlet_1">
      <servlet-name>calendarDemo</servlet-name>
      <jsp-file>/calendarDemo.jsp</jsp-file>
   </servlet>
   <servlet id="Servlet_2">
      <servlet-name>CalendarInit</servlet-name>
      <servlet-class>CalendarInit</servlet-class>
      <load-on-startup>1</load-on-startup>
   </servlet>
   <taglib>
      <taglib-uri>utils</taglib-uri>
      <taglib-location>/WEB-INF/tlds/utils.tld</taglib-location>
   </taglib>
</web-app>

Note that we load CalendarInit on startup. That's because, as you'll see further on, we perform the expensive home interface lookups and store them in application scope. Here's the utils.tld file:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd">
<taglib>
	<tag>
		<name>setDate</name>
		<tagclass>tags.SetDate</tagclass>
		<bodycontent>empty</bodycontent>
	</tag>
	<tag>
		<name>showCalendar</name>
		<tagclass>tags.GenerateCalendar</tagclass>
		<bodycontent>empty</bodycontent>
	</tag>
	<tag>
		<name>showNavigation</name>
		<tagclass>tags.GenerateNavigation</tagclass>
		<bodycontent>empty</bodycontent>
	</tag>
</taglib>

No surprises here. Before looking at the tag code, let's take a quick look at the CalendarInit code:

import	java.io.FileWriter;
import	java.io.PrintWriter;
import	java.util.Properties;
import	javax.naming.InitialContext;
import	javax.naming.Context;
import	javax.naming.NamingException;
import	java.rmi.RemoteException;
import	javax.ejb.EJBException;
import	com.penguinpools.CalendarHome;
import	com.penguinpools.TemperaturesHome;
import	com.penguinpools.LocationHome;
import	javax.servlet.jsp.PageContext;
import	javax.servlet.Servlet;
import	javax.servlet.ServletConfig;
import	javax.servlet.ServletContext;
import	javax.servlet.ServletRequest;
import	javax.servlet.ServletResponse;

public class CalendarInit implements Servlet {

	private ServletConfig	conf;

	public void init( ServletConfig conf ) {
		this.conf = conf;
		ServletContext	ctx = conf.getServletContext();
		try {
			Object	obj = null;

			Properties props = new Properties();
			props.put( Context.PROVIDER_URL, "iiop://localhost:9001" );
			props.put( Context.INITIAL_CONTEXT_FACTORY,
			  "com.ibm.websphere.naming.WsnInitialContextFactory" );
			Context context = new InitialContext( props );
			obj = context.lookup( "penguinpools/Location" );
			LocationHome locationHome = (LocationHome)
			  javax.rmi.PortableRemoteObject.narrow( obj,
			  LocationHome.class );
			ctx.setAttribute( "locationHome", locationHome );
			obj = context.lookup( "penguinpools/Calendar" );
			CalendarHome calendarHome = (CalendarHome)
			  javax.rmi.PortableRemoteObject.narrow( obj,
			  CalendarHome.class );
			ctx.setAttribute( "calendarHome", calendarHome );
			obj = context.lookup( "penguinpools/Temperatures" );
			TemperaturesHome temperaturesHome = (TemperaturesHome)
			  javax.rmi.PortableRemoteObject.narrow( obj,
			  TemperaturesHome.class );
			ctx.setAttribute( "temperaturesHome", temperaturesHome );
		}
		catch( Exception e ) {
			e.printStackTrace();
		}
	}

	public ServletConfig getServletConfig() {
		return( conf );
	}

	public String getServletInfo() {
		return( null );
	}

	public void service( ServletRequest req, ServletResponse resp ) {
	}

	public void destroy() {
	}
}

Basic stuff, performing the lookups and then storing object references to the home interface in application scope. Odd? Not really. I find that naming service lookups can take 1100 milliseconds or more. If you're doing that three times, as I do here, you're going to take a considerable performance hit if you do it every time you load the page.

Here's the code for SetDate:

package	tags;

import	java.io.IOException;
import	java.util.Calendar;
import	java.util.GregorianCalendar;
import	java.text.DateFormat;
import	java.util.StringTokenizer;
import	javax.servlet.jsp.tagext.TagSupport;
import	javax.servlet.jsp.JspException;
import	javax.servlet.jsp.JspWriter;
import	javax.servlet.jsp.PageContext;
import	javax.servlet.http.HttpServletRequest;

public class SetDate extends TagSupport {

	private static final String	months[] = {
	  "January", "February", "March", "April", "May", "June", "July",
	  "August", "September", "October", "November", "December" };

	public int doStartTag() throws JspException {
		HttpServletRequest	req = null;
		GregorianCalendar	cal = null;
		StringTokenizer	st = null;
		String		cmd = null;
		int		i = -1;

		cal = (GregorianCalendar) pageContext.getAttribute( "date",
		  PageContext.SESSION_SCOPE );
		if( cal == null ) {
			cal = new GregorianCalendar();
			cal.set( Calendar.DATE, 1 );
			pageContext.setAttribute( "date", cal,
			  PageContext.SESSION_SCOPE );
		}
		req = (HttpServletRequest) pageContext.getRequest();
		cmd = req.getParameter( "submit" );
		if( cmd != null ) {
			if( cmd.equals( "<<<" ) )
				cal.roll( Calendar.YEAR, -1 );
			else if( cmd.equals( "<<" ) ) {
				if( cal.get( Calendar.MONTH ) == 0 )
					cal.roll( Calendar.YEAR, -1 );
				cal.roll( Calendar.MONTH, -1 );
			}
			else if( cmd.equals( "---" ) ) {
				cal = new GregorianCalendar();
				cal.set( Calendar.DATE, 1 );
			}
			else if( cmd.equals( ">>>" ) )
				cal.roll( Calendar.YEAR, 1 );
			else if( cmd.equals( ">>" ) ) {
				if( cal.get( Calendar.MONTH ) == 11 )
					cal.roll( Calendar.YEAR, 1 );
				cal.roll( Calendar.MONTH, 1 );
			}
			pageContext.setAttribute( "date", cal,
			  PageContext.SESSION_SCOPE );
		}
		try {
			generate( pageContext.getOut(), cal );
		}
		catch( IOException e ) {
			throw( new JspException( getClass().getName() + ": " +
			  e.toString() ) );
		}
		return( SKIP_BODY );
	}

	private void generate( JspWriter out, GregorianCalendar cal )
	  throws IOException {
		int		i = -1;
		int		mon = -1;

		out.println( "<h1>Calendar for " +
		  months[cal.get( Calendar.MONTH )] + " " +
		  cal.get( Calendar.YEAR ) + "</h1>" );
	}
}

Fairly straightforward stuff. We pull a GregorianCalendar from session scope, or create a new one if not found, handle the navigation buttons and display a header. Notice the coupling between this class and the GenerateNavigation class:

package	tags;

import	java.io.IOException;
import	java.util.Calendar;
import	java.util.GregorianCalendar;
import	java.text.DateFormat;
import	java.util.StringTokenizer;
import	javax.servlet.jsp.tagext.TagSupport;
import	javax.servlet.jsp.JspException;
import	javax.servlet.jsp.JspWriter;
import	javax.servlet.jsp.PageContext;

public class GenerateNavigation extends TagSupport {

	public int doStartTag() throws JspException {
		try {
			generate( pageContext.getOut() );
		}
		catch( IOException e ) {
			throw( new JspException( getClass().getName() + ": " +
			  e.toString() ) );
		}
		return( SKIP_BODY );
	}

	private void generate( JspWriter out )
	  throws IOException {
		String	prefix = "<td class=\"navigation\"><input type=\"submit\" name=\"submit\" " +
		  "value = \"";
		String	suffix = "\"></td>";

		out.println( "<tr>" );
		out.println( "<th class=\"navigation\">Backward<br>" +
		  "a year</th>" );
		out.println( "<th class=\"navigation\">Backward<br>" +
		  "a month</th>" );
		out.println( "<th class=\"navigation\">Reset to<br>" +
		  "current</th>" );
		out.println( "<th class=\"navigation\">Forward<br>" +
		  "a month</th>" );
		out.println( "<th class=\"navigation\">Forward<br>" +
		  "a year</th>" );
		out.println( "</tr>" );
		out.println( "<tr>" );
		out.println( prefix + "<<<" + suffix );
		out.println( prefix + "<<" + suffix );
		out.println( prefix + "---" + suffix );
		out.println( prefix + ">>" + suffix );
		out.println( prefix + ">>>" + suffix );
		out.println( "</tr>" );
	}
}

Nothing remarkable here. We generate a table so everything gets rendered nicely and set the appropriate button values which are processed by SetDate. Note that I specify the class in the table headers; this permits customization by the HTML author. Finally, here's the gist of the application: the GenerateCalendar class:

package	tags;

import	java.io.IOException;
import	java.sql.SQLException;
import	java.util.GregorianCalendar;
import	java.text.DateFormat;
import	java.text.DecimalFormat;
import	java.util.StringTokenizer;
import	java.util.Vector;
import	javax.servlet.jsp.tagext.TagSupport;
import	javax.servlet.jsp.JspException;
import	javax.servlet.jsp.JspWriter;
import	javax.servlet.jsp.PageContext;
import	com.penguinpools.Calendar;
import	com.penguinpools.CalendarHome;
import	com.penguinpools.CalendarObject;
import	com.penguinpools.Temperatures;
import	com.penguinpools.TemperaturesHome;
import	com.penguinpools.TemperaturesObject;

public class GenerateCalendar extends TagSupport {

	public int doStartTag() throws JspException {
		GregorianCalendar	cal = null;
		CalendarHome		times = null;
		TemperaturesHome	temps = null;

		cal = (GregorianCalendar) pageContext.getAttribute( "date",
		  PageContext.SESSION_SCOPE );
		if( cal == null )
			throw( new JspException( "date missing from page" ) );
		times = (CalendarHome) pageContext.getAttribute( "calendarHome",
		  PageContext.APPLICATION_SCOPE );
		if( times == null )
			throw( new JspException( "times missing from page" ) );
		temps = (TemperaturesHome) pageContext.getAttribute(
		  "temperaturesHome", PageContext.APPLICATION_SCOPE );
		if( temps == null )
			throw( new JspException( "temps missing from page" ) );
		try {
			generate( pageContext.getOut(), cal, times.create(),
			  temps.create() );
		}
		catch( Exception e ) {
			String	msg = e.getMessage();
			if( msg == null )
				msg = e.toString();
			throw( new JspException( getClass().getName() + ": " +
			  msg ) );
		}
		return( SKIP_BODY );
	}

	private void generate( JspWriter out, GregorianCalendar cal,
	  Calendar time, Temperatures temp )
	  throws IOException, SQLException {
		int		i = -1;
		int		mon = -1;
		Vector		times = time.listMonth( 998,
		  cal.get( GregorianCalendar.YEAR ),
		  cal.get( GregorianCalendar.MONTH ) );
		CalendarObject	objTime = null;
		Vector		temps = temp.listMonth( 998,
		  cal.get( GregorianCalendar.MONTH ) );
		TemperaturesObject	objTemp = null;
		DecimalFormat	df = new DecimalFormat( "0.00" );

		cal.set( GregorianCalendar.DATE, 1 );
		out.println( "<tr>" );
		out.println( "<th width=\"15%\" class=\"calendar\">Sun.</th>" );
		out.println( "<th width=\"14%\" class=\"calendar\">Mon.</th>" );
		out.println( "<th width=\"14%\" class=\"calendar\">Tue.</th>" );
		out.println( "<th width=\"14%\" class=\"calendar\">Wed.</th>" );
		out.println( "<th width=\"14%\" class=\"calendar\">Thu.</th>" );
		out.println( "<th width=\"14%\" class=\"calendar\">Fri.</th>" );
		out.println( "<th width=\"15%\" class=\"calendar\">Sat.</th>" );
		out.println( "</tr>" );
		out.println( "<tr>" );
		for( i = 1; i <= 7; i++ ) {
			out.print( getHeader( i ) );
			if( i == cal.get( GregorianCalendar.DAY_OF_WEEK ) ) {
				out.print( cal.get( GregorianCalendar.DATE ) );
				objTime = (CalendarObject) times.elementAt(
				  cal.get( GregorianCalendar.DATE ) - 1 );
				if( objTime != null ) {
					out.println( "<br>" );
					out.print( "<p class=\"sunrise\">" );
					out.print( objTime.cal_sunrise );
					out.print( "</p>" );
					out.print( "<p class=\"slash\">" );
					out.print( "/</p>" );
					out.print( "<p class=\"sunset\">" );
					out.print( objTime.cal_sunset );
					out.println( "</font>" );
				}
				objTemp = (TemperaturesObject) temps.elementAt(
				  cal.get( GregorianCalendar.DATE ) - 1 );
				if( objTime != null ) {
					out.println( "<br>" );
					out.print( "<p class=\"temphigh\">" );
					out.print( df.format( objTemp.temp_high.doubleValue() ) );
					out.print( "</p>" );
					out.print( "<p class=\"slash\">" );
					out.print( "/</p>" );
					out.print( "<p class=\"templow\">" );
					out.print( df.format( objTemp.temp_high.doubleValue() ) );
					out.println( "</p>" );
				}
				cal.add( GregorianCalendar.DATE, 1 );
			}
			else
				out.println( " " );
			out.println( "</td>" );
		}
		out.println( "</tr>" );
		mon = cal.get( GregorianCalendar.MONTH );
		while( mon == cal.get( GregorianCalendar.MONTH ) ) {
			out.println( "<tr>" );
			for( i = 1; i <= 7; i++ ) {
				out.print( getHeader( i ) );
				if( mon != cal.get( GregorianCalendar.MONTH ) ) {
					out.println( " " );
					out.println( "</td>" );
					continue;
				}
				out.print( cal.get( GregorianCalendar.DATE ) );
				objTime = (CalendarObject) times.elementAt(
				  cal.get( GregorianCalendar.DATE ) - 1 );
				if( objTime != null ) {
					out.println( "<br>" );
					out.print( "<p class=\"sunrise\">" );
					out.print( objTime.cal_sunrise );
					out.print( "</p>" );
					out.print( "<p class=\"slash\">" );
					out.print( "/</p>" );
					out.print( "<p class=\"sunset\">" );
					out.print( objTime.cal_sunset );
					out.println( "</font>" );
				}
				objTemp = (TemperaturesObject) temps.elementAt(
				  cal.get( GregorianCalendar.DATE ) - 1 );
				if( objTime != null ) {
					out.println( "<br>" );
					out.print( "<p class=\"temphigh\">" );
					out.print( df.format( objTemp.temp_high.doubleValue() ) );
					out.print( "</p>" );
					out.print( "<p class=\"slash\">" );
					out.print( "/</p>" );
					out.print( "<p class=\"templow\">" );
					out.print( df.format( objTemp.temp_high.doubleValue() ) );
					out.println( "</p>" );
				}
				cal.add( GregorianCalendar.DATE, 1 );
				out.println( "</td>" );
			}
			out.println( "</tr>" );
		}
		cal.set( GregorianCalendar.MONTH, mon );
		cal.set( GregorianCalendar.DATE, 1 );
	}

	private String getHeader( int dayOfWeek ) {
		String	style = "weekday";

		if( ( dayOfWeek == 1 ) || ( dayOfWeek == 7 ) )
			style = "weekend";
		return( "<td class=\"" + style + "\">" );
	}
}

Nothing really surprising here. You might note that the doStartTag method doesn't do much. It's a direct result of the way I write and test my code. The generate method came from a stand-alone application. The stand-alone application contains the lookup code (which is incorporated into CalendarInit) and calls generate with the appropriate parameters. After I've got everything tested and debugged then I copy the code to the corresponding classes.

Note that I've taken care to identify the text elements in the generated table with the class attribute. If you go back to the JSP source then you'll see how we use a cascading style sheet to control the presentation of the data. Giving the web page author control over appearance of the table is vital. It's a lot easier to change details like background and font colours in a JSP rather than in the underlying EJBs.

So where's the code for the EJBs which interface with the database? Anyone who has worked with EJBs knows that there are a number of files for each class, namely the remote, home and bean interfaces. My automatic generation program also creates a convenient wrapper object. Rather than listing all of the classes involved, I'll just include the CalendarBean source:

package	com.penguinpools;

import	java.sql.Connection;
import	java.sql.PreparedStatement;
import	java.sql.Statement;
import	java.sql.SQLException;
import	java.sql.ResultSet;
import	java.io.ByteArrayInputStream;
import	java.io.InputStream;
import	java.io.IOException;
import	java.sql.Date;
import	java.text.DecimalFormat;
import	java.util.Hashtable;
import	java.util.Vector;
import	java.util.Enumeration;
import	javax.ejb.*;
import	javax.naming.*;
import	javax.sql.DataSource;
import	java.util.GregorianCalendar;
import	com.penguinpools.Calendar;
import	com.penguinpools.CalendarObject;
import	java.io.OutputStreamWriter;
import	java.io.InputStreamReader;
import	java.io.BufferedReader;
import	java.io.PrintWriter;
import	java.io.FileWriter;
import	java.net.URL;
import	java.net.URLConnection;
import	java.util.Vector;
import	java.util.StringTokenizer;
import	java.util.GregorianCalendar;
import	javax.naming.InitialContext;
import	javax.naming.Context;
import	javax.naming.NamingException;
import	java.rmi.RemoteException;
import	com.penguinpools.CalendarObject;
import	com.penguinpools.Location;
import	com.penguinpools.LocationHome;
import	com.penguinpools.LocationBean;
import	com.penguinpools.LocationObject;
import	com.penguinpools.NoRecordFoundException;

/**
 * This is the implementation of the Calendar class.
 * @see com.penguinpools.Calendar
 * @author Phil Selby, December 9th, 2002
 * Copyright © 2002 by Phil Selby. All rights reserved.
 */

public class CalendarBean implements javax.ejb.SessionBean {

	public SessionContext	context = null;
	private static final DecimalFormat	fmt0 = new DecimalFormat( "0000" );

	public void create( java.sql.Connection conn, Integer location_id, GregorianCalendar cal_date, String cal_sunrise, String cal_sunset ) throws SQLException {
		Statement	stmt = null;
		DecimalFormat	df2 = new DecimalFormat( "00" );
		DecimalFormat	df4 = new DecimalFormat( "0000" );

		String	insert = "INSERT INTO CALENDAR VALUES (  " + location_id + ", TO_DATE( '" + df2.format( cal_date.get( GregorianCalendar.MONTH ) + 1 ) + "-" + df2.format( cal_date.get( GregorianCalendar.DATE ) ) + "-" + df4.format( cal_date.get( GregorianCalendar.YEAR ) ) + "', 'MM-DD-YYYY' ), '" + cal_sunrise + "', '" + cal_sunset + "' )";

		try {
			stmt = conn.createStatement();
			stmt.executeUpdate( insert );
		}
		finally {
			if( stmt != null ) try { stmt.close(); } catch( Exception f ) {};
		}
	}

	public void create( Integer location_id, GregorianCalendar cal_date, String cal_sunrise, String cal_sunset ) throws SQLException {
		Connection	conn = null;

		try {
			conn = getConnection();
			create( conn, location_id, cal_date, cal_sunrise, cal_sunset );
		}
		finally {
			if( conn != null )
				try { conn.close(); } catch( Exception f ) {};
		}
	}

	public void delete( Integer location_id, GregorianCalendar cal_date ) throws SQLException {
		Connection	conn = null;
		DecimalFormat	df2 = new DecimalFormat( "00" );
		DecimalFormat	df4 = new DecimalFormat( "0000" );
		String	delete = "DELETE FROM CALENDAR WHERE LOCATION_ID = " + location_id + " AND CAL_DATE = TO_DATE( '" + df2.format( cal_date.get( GregorianCalendar.MONTH ) + 1 ) + "-" + df2.format( cal_date.get( GregorianCalendar.DATE ) ) + "-" + df4.format( cal_date.get( GregorianCalendar.YEAR ) ) + "', 'MM-DD-YYYY' )";
		Statement	stmt = null;

		try {
			conn = getConnection();
			stmt = conn.createStatement();
			stmt.executeUpdate( delete );
		}
		finally {
			if( stmt != null ) try { stmt.close(); } catch( Exception f ) {}
			if( conn != null ) try { conn.close(); } catch( Exception f ) {}
		}
	}

	public CalendarObject request( int cmd, CalendarObject obj ) throws SQLException {

		switch( cmd ) {

		case Calendar.INSERT:
			create( obj.location_id, obj.cal_date, obj.cal_sunrise, obj.cal_sunset );
			break;

		case Calendar.SELECT:
			obj = getRecord( obj.location_id, obj.cal_date );
			break;

		case Calendar.UPDATE:
			putRecord( obj.location_id, obj.cal_date, obj.cal_sunrise, obj.cal_sunset );
			break;

		case Calendar.DELETE:
			delete( obj.location_id, obj.cal_date );
			break;

		}
		return( obj );
	}

	public void setCalSunrise( Integer location_id, GregorianCalendar cal_date, String cal_sunrise ) throws SQLException {
		putRecord( location_id, cal_date, cal_sunrise, null );
	}

	public String getCalSunrise( Integer location_id, GregorianCalendar cal_date ) throws SQLException {
		return( ( (CalendarObject) getRecord( location_id, cal_date ) ).cal_sunrise );
	}

	public void setCalSunset( Integer location_id, GregorianCalendar cal_date, String cal_sunset ) throws SQLException {
		putRecord( location_id, cal_date, null, cal_sunset );
	}

	public String getCalSunset( Integer location_id, GregorianCalendar cal_date ) throws SQLException {
		return( ( (CalendarObject) getRecord( location_id, cal_date ) ).cal_sunset );
	}

	private CalendarObject getRecord( Integer location_id, GregorianCalendar cal_date ) throws SQLException {
		Connection	conn = null;
		Statement	stmt = null;
		DecimalFormat	df2 = new DecimalFormat( "00" );
		DecimalFormat	df4 = new DecimalFormat( "0000" );
		ResultSet	rs = null;
		CalendarObject	obj = null;

		obj = new CalendarObject();
		String	query = "SELECT location_id, cal_date, cal_sunrise, cal_sunset FROM CALENDAR WHERE LOCATION_ID = " + location_id + " AND CAL_DATE = TO_DATE( '" + df2.format( cal_date.get( GregorianCalendar.MONTH ) + 1 ) + "-" + df2.format( cal_date.get( GregorianCalendar.DATE ) ) + "-" + df4.format( cal_date.get( GregorianCalendar.YEAR ) ) + "', 'MM-DD-YYYY' )";
		InputStream	is = null;

		try {
			conn = getConnection();
			stmt = conn.createStatement();
			rs = stmt.executeQuery( query );
			if( !rs.next() )
				throw( new com.penguinpools.NoRecordFoundException() );
			obj.location_id = new Integer( rs.getInt( 1 ) );
			if( rs.wasNull() )
				obj.location_id = null;
			obj.cal_date = new GregorianCalendar();
			obj.cal_date.setTime( rs.getDate( 2 ) );
			if( rs.wasNull() )
				obj.cal_date = null;
			obj.cal_sunrise = rs.getString( 3 );
			if( rs.wasNull() )
				obj.cal_sunrise = null;
			obj.cal_sunset = rs.getString( 4 );
			if( rs.wasNull() )
				obj.cal_sunset = null;
			rs.close();
		}
		finally {
			if( rs != null ) try { rs.close(); } catch( Exception f ) {}
			if( stmt != null ) try { stmt.close(); } catch( Exception f ) {}
			if( conn != null ) try { conn.close(); } catch( Exception f ) {}
		}
		return( obj );
	}

	public int putRecord( Integer location_id, GregorianCalendar cal_date, String cal_sunrise, String cal_sunset ) throws SQLException {
		Statement	stmt = null;
		Connection	conn = null;
		StringBuffer	update = new StringBuffer();
		int	count = 0;

		update.append( "UPDATE CALENDAR SET " );
		if( cal_sunrise != null ) {
			if( count > 0 )
				update.append( ", " );
			update.append( "CAL_SUNRISE = '" + cal_sunrise + "'" );
			count++;
		}
		if( cal_sunset != null ) {
			if( count > 0 )
				update.append( ", " );
			update.append( "CAL_SUNSET = '" + cal_sunset + "'" );
			count++;
		}
		update.append( " WHERE LOCATION_ID = " + location_id + " AND CAL_DATE = '" + cal_date + "'" );
		if( count == 0 )
			return( 0 );
		int	rc = 0;
		try {
			conn = getConnection();
			stmt = conn.createStatement();
			int	index = 1;
			rc = stmt.executeUpdate( update.toString() );
		}
		finally {
			if( stmt != null ) try { stmt.close(); } catch( Exception f ) {}
			if( conn != null ) try { conn.close(); } catch( Exception f ) {}
		}
		return( rc );
	}

	private static final DecimalFormat	df2 = new DecimalFormat( "00" );
	private static final DecimalFormat	df4 = new DecimalFormat( "0000" );

	/*
	 * return a Vector of CalendarObjects containing sunrise/sunset
	 * data for the specified location and month
	 */

	public Vector listMonth( int location_id, int year, int month )
	  throws SQLException, IllegalArgumentException {
		Connection	conn = null;
		Statement	stmt = null;
		ResultSet	rs = null;
		CalendarObject	obj = null;
		GregorianCalendar	cal = null;
		Vector		result = new Vector();
		int		count = 0;

		/*
		 * check for valid month
		 */

		if( ( month < 0 ) || ( month > 11 ) )
			throw( new IllegalArgumentException( month +
			  ": month must be between 0 and 11" ) );

		/*
		 * build the query string
		 */

		String	query = "SELECT COUNT(*) FROM CALENDAR " +
		  "WHERE cal_date >= TO_DATE( '" + df2.format( month + 1 ) +
		  "-" + df2.format( 1 ) + "-" + df4.format( year ) +
		  "', 'MM-DD-YYYY' ) AND cal_date < TO_DATE( '";
		if( month == 11 ) {
			month = 0;
			year++;
		}
		else
			month++;
		query += df2.format( month + 1 ) + "-" + df2.format( 1 ) +
		  "-" + df4.format( year ) + "', 'MM-DD-YYYY' ) " +
		  " ORDER BY cal_date";
		try {
			conn = getConnection();
			stmt = conn.createStatement();

			/*
			 * execute the count query and populate the
			 * database as necessary
			 */

			rs = stmt.executeQuery( query );
			if( ! rs.next() )
				throw( new EJBException( "SELECT COUNT failed" ) );
			count = rs.getInt( 1 );
			rs.close();
			stmt.close();
			if( count == 0 )
				populate( location_id, year );

			/*
			 * rebuild the query with the field names
			 */

			int	index = query.indexOf( "COUNT(*)" );
			query = query.substring( 0, index ) +
			  "location_id, cal_date, cal_sunrise, cal_sunset" +
			  query.substring( index + 8 );
			stmt = conn.createStatement();
			rs = stmt.executeQuery( query );

			/*
			 * build the result vector
			 */

			while( rs.next() ) {
				obj = new CalendarObject();
				obj.location_id = new Integer( rs.getInt( 1 ) );
				obj.cal_date = new GregorianCalendar();
				obj.cal_date.setTime( rs.getDate( 2 ) );
				obj.cal_sunrise = rs.getString( 3 );
				if( rs.wasNull() )
					obj.cal_sunrise = null;
				obj.cal_sunset = rs.getString( 4 );
				if( rs.wasNull() )
					obj.cal_sunset = null;
				result.add( obj );
			}
		}
		finally {
			if( rs != null ) try { rs.close(); } catch( Exception f ) {}
			if( stmt != null ) try { stmt.close(); } catch( Exception f ) {}
			if( conn != null ) try { conn.close(); } catch( Exception f ) {}
		}
		if( result.size() == 0 )
			throw( new NoRecordFoundException() );
		return( result );
	}

	private static final String	host = "mach.usno.navy.mil";
	private static final String	dir = "/cgi-bin/aa_rstablew.pl";

	/*
	 * populate the database table for a particular location and year
	 */

	private void populate( int location, int year ) {
		URL			url = null;
		URLConnection		conn = null;
		StringBuffer		query = new StringBuffer();
		String			result = null;
		OutputStreamWriter	sw = null;
		BufferedReader		br = null;
		DecimalFormat		df = new DecimalFormat( "00 " );
		Number			day = null;
		StringTokenizer		st = null;
		String			token = null;
		String			sunrise = null;
		String			sunset = null;
		GregorianCalendar	dateValue = null;
		CalendarObject		req = new CalendarObject();
		com.penguinpools.Calendar	bean = null;
		LocationObject		obj = new LocationObject();
		Context			context = null;
		Integer			loc = new Integer( location );

		/*
		 * get an initial context
		 */

		try {
			context = new InitialContext();
		}
		catch( NamingException e ) {
			throw( new EJBException( e ) );
		}

		/*
		 * get the location record
		 */

		try {
			Object	o = context.lookup( "penguinpools/Location" );
			LocationHome home = (LocationHome)
			  javax.rmi.PortableRemoteObject.narrow( o,
			  LocationHome.class );
			Location	lbean = home.create();
			obj.location_id = loc;
			obj = lbean.request( Location.SELECT, obj );
		}
		catch( Exception e ) {
			throw( new EJBException( e ) );
		}

		/*
		 * check that we have lat/long information
		 */

		if( ( obj == null ) || ( obj.location_lat == null ) ||
		  ( obj.location_long == null ) )
			throw( new EJBException( "location data missing" ) );

		/*
		 * get a calendar bean
		 */

		try {
			Object	o = context.lookup( "penguinpools/Calendar" );
			CalendarHome home = (CalendarHome)
			  javax.rmi.PortableRemoteObject.narrow( o,
			  CalendarHome.class );
			bean = home.create();
		}
		catch( Exception e ) {
			throw( new EJBException( e ) );
		}

		/*
		 * create the URL and the connection
		 */

		try {
			url = new URL( "http://" + host + dir );
			conn = url.openConnection();
			conn.setDoOutput( true );
			conn.setRequestProperty( "Content-Type",
			  "application/x-www-form-urlencoded" );
			query.append( "rrx=2&" );
			query.append( "year=" + year + "&" );
			query.append( "type=0&" );
			query.append( "xxplace=Burlington&" );
			if( obj.location_long.doubleValue() < 0 )
				query.append( "xx0=-1&" );
			else
				query.append( "xx0=1&" );
			query.append( "xx1=" + getWhole( obj.location_long ) +
			  "&" );	// obj.location_long
			query.append( "xx2=" + getFract( obj.location_long ) +
			  "&" );	// minutes
			if( obj.location_lat.doubleValue() < 0 )
				query.append( "yy0=-11&" );
			else
				query.append( "yy0=1&" );
			query.append( "yy1=" + getWhole( obj.location_lat ) +
			  "&" );	// obj.location_lat
			query.append( "yy2=" + getFract( obj.location_lat ) +
			  "&" );	// minutes
			query.append( "zz0=-1&" );	// timezone
			query.append( "zz1=5\r\n" );
			sw = new OutputStreamWriter( conn.getOutputStream() );
			sw.write( query.toString() );
			sw.flush();
			sw.close();

			/*
			 * read the results
			 */

			br = new BufferedReader( new InputStreamReader( conn.getInputStream() ) );
			while( ( result = br.readLine() ) != null ) {
				if( result.length() < 3 )
					continue;
				token = result.substring( 0, 3 );

				/*
				 * we use a DecimalFormat instance to
				 * check for lines starting with two
				 * numeric characters followed by blank
				 */

				try {
					day = df.parse( token );
				}
				catch( Exception e ) {
					continue;
				}
				result = result.substring( 2 );
				for( int i = 0; i < 12; i++ ) {

					/*
					 * slice and dice the output
					 * and add to the database
					 */

					result = result.substring( 2 );
					token = result.substring( 0, 4 );
					sunrise = null;
					if( ! isBlank( token ) )
						sunrise = token.trim();
					result = result.substring( 5 );
					token = result.substring( 0, 4 );
					sunset = token.trim();
					result = result.substring( 4 );
					if( sunrise == null )
						continue;
					req.location_id = loc;
					req.cal_date = new GregorianCalendar(
					  year, i, day.intValue() );
					req.cal_sunrise = fmtTime( sunrise,
					  req.cal_date );
					req.cal_sunset = fmtTime( sunset,
					  req.cal_date );
					bean.request( com.penguinpools.Calendar.INSERT, req );
				}
			}
			br.close();
		}
		catch( Exception e ) {
			throw( new EJBException( e ) );
		}
	}

	private static boolean isBlank( String s ) {
		for( int i = 0; i < s.length(); i++ )
			if( s.charAt( i ) != ' ' )
				return( false );
		return( true );
	}

	private static String getWhole( Double D ) {
		double	d = D.doubleValue();
		DecimalFormat	df = new DecimalFormat( "0.00" );

		if( d < 0 )
			d = 0 - d;
		String	s = df.format( d );
		return( s.substring( 0, s.indexOf( '.' ) ) );
	}

	private static String getFract( Double D ) {
		DecimalFormat	df = new DecimalFormat( "0.00" );

		String	s = df.format( D.doubleValue() );
		return( s.substring( s.indexOf( '.' ) + 1 ) );
	}

	private static String fmtTime( String s, GregorianCalendar cal ) {
		int	hour = 0;
		int	minute = 0;
		while( s.length() < 4 )
			s = "0" + s;
		for( int i = 0; i < 4; i++ ) {
			if( i < 2 ) {
				hour *= 10;
				hour += ( s.charAt( i ) - '0' );
			}
			else {
				minute *= 10;
				minute += ( s.charAt( i ) - '0' );
			}
		}
		if( isDST( cal ) )
			hour++;
		DecimalFormat	df = new DecimalFormat( "00" );
		return( df.format( hour ) + ":" + df.format( minute ) );
	}

	/*
	 * figure out if we're on daylight saving time
	 */

	private static boolean isDST( GregorianCalendar cal ) {
		int	month = cal.get( GregorianCalendar.MONTH );
		int	day = cal.get( GregorianCalendar.DATE );
		if( ( month < 3 ) || ( month > 9 ) )
			return( false );
		if( ( month > 3 ) && ( month < 9 ) )
			return( true );
		GregorianCalendar	cusp = null;

		/*
		 * This is actually kind of neat behaviour for the
		 * GregorianCalendar class. By setting the day of
		 * the week then the day of week in month I can
		 * specify things like the first or last Sunday of
		 * the month, precisely what we need.
		 */

		if( month == 3 ) {
			cusp = new GregorianCalendar(
		  	  cal.get( GregorianCalendar.YEAR ), 3, 1 );
			cusp.set( GregorianCalendar.DAY_OF_WEEK,
			  GregorianCalendar.SUNDAY );
			cusp.set( GregorianCalendar.DAY_OF_WEEK_IN_MONTH, 1 );
			if( day >= cusp.get( GregorianCalendar.DATE ) )
				return( true );
			return( false );
		}
		cusp = new GregorianCalendar(
	  	  cal.get( GregorianCalendar.YEAR ), 9, 1 );
		cusp.set( GregorianCalendar.DAY_OF_WEEK,
		  GregorianCalendar.SUNDAY );
		cusp.set( GregorianCalendar.DAY_OF_WEEK_IN_MONTH, -1 );
		if( day < cusp.get( GregorianCalendar.DATE ) )
			return( true );
		return( false );
	}

	private Connection getConnection() throws SQLException {
		try {
			Context context = new InitialContext();
			String dsName = (String) context.lookup(
			  "java:comp/env/DSname" );
			DataSource ds = (DataSource)
			  context.lookup( dsName );
			return( ds.getConnection() );
		}
		catch( NamingException e ) {
			throw new EJBException( e );
		}
	}

	public void ejbCreate() {
	}

	public void ejbRemove() {
	}

	public void setSessionContext( SessionContext context ) {
		this.context = context;
	}

	public void ejbActivate() {
	}

	public void ejbPassivate() {
	}
}

There's some extraneous code here but that's because I use a generator for all of my beans. It's driven by the structure of the underlying database and provides additional functionality which isn't required in this case. We only invoke the listMonth method in our demonstration. It returns a vector of CalendarObjects, sorted ascendingly by date. That's why we can use elementAt( date ) to get the entry for a particular day of the month.

Note the populate method. That's where we go out to the U.S. Naval Observatory and pull the sunrise and sunset times for years which don't already exist in the database. Realistically, the site or path or format of the data might change. I'd prefer if they could make the information available in an XML document. The philosophy is what's important in this case: having the application automatically retrieve data needed to serve to clients.

I haven't included things like the ejb-jar.xml or any of the other beans and interfaces. There's enough here to demonstrate the approach. If you have a particular application in mind, drop me a note and we'll see what we can do.

Copyright © 2002 by Phil Selby. All rights reserved.