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.