[Image] EchoPoint
Helping you build truly dynamic and stateful web applications!
nothing

Using JspComponentPeer - The alternative way to building UI peers

By Brad Baker
Wednesday, May 05, 2004

 

If you need to extend the behavior of an Echo component, often its enough to just extend the component class and add the extra features you want. 

However if you need to really replace the way the component is visually rendered or there is no Echo/EchoPoint component that meets your visual needs, then you end up having to write your own component and component peer.

The component peer object is responsible for rendering the HTML that represents the component on the client browser.  Up until now you needed to use the very wordy nextapp.echoservlet.html.* classes to create the HTML markup that represents your component.

Well now there is another way, and that's doing it via a JSP page and the new echopoint.ui.jsp.JspComponentPeer class.

Notice I didn't say better way!  I don't advocate the JSP page approach over the Java code approach.  There are pros and cons for each.

Its up to you to decide if it appeals to you as a component developer.  But if you have just come in from the cold cold regions of paged based web application development, the JSP approach may seem very natural indeed.  Or if you need extremely precise HTML generation, chock full of invisible gifs of just the right height, then the new JSP approach might be just the ticket.

First off you need to be familiar in how to create your own nextapp.echoservlet.ComponentPeer.  For a detailed examination of this look at this article from NextApp as well as this article on the EchoPoint site(Both open new windows)

Then once you have read and understood them, come back this article and do it in a completely different way.

The Java to HTML Issue

When you begin to create ComponentPeers using the nextapp.echoservlet.html.Element and its associated classes, you may quickly find that creating complex and "finely tuned" HTML output can become quite difficult.  Simple component peers  like this :


public void render(RenderingContext rc, Element parent) {
HorizontalRule hr = (HorizontalRule) getComponent();
Element element = new Element("hr", false);
element.addAttribute("size", hr.getSize());
parent.addElement(element);
}

look innocent enough.  But <hr size="<%=hr.getSize() %>"> is much more terse and HTML like.

When you start building quite complex components, this can quickly get out of hand.  For example take a look at this Java code extract from the echopoint.ui.SortableTableRenderer, the class responsible for rendering a echopoint.SortableTable.

(In fact you can quickly skip it, cause its just that long.  Really its very long.  Just scroll down to the end of the green lines....)

  //----------------------------------------------------
  // Render our inner table
  //----------------------------------------------------
  Element tableElement = new Element(ElementNames.TABLE);
  tableElement.addAttribute(ElementNames.Attributes.BORDER, 0);
  tableElement.addAttribute(ElementNames.Attributes.CELLPADDING, table.getCellMargin());
  tableElement.addAttribute(ElementNames.Attributes.CELLSPACING, table.getBorderSize());

  // Generate <colgroup> element (only added if necessary).
  // Determine if all columns have widths (set the allColumnsHaveWidths property,
  // which will be used later.
  Element colGroupElement = new Element(ElementNames.COLGROUP);
  int totalColumnWidths = 0;
  for (int column = 0; column < columnCount; column++) {
   TableColumn tableColumn = columnModel.getColumn(column);
   int width = tableColumn.getWidth();
   totalColumnWidths += width;
   Element colElement = new Element(ElementNames.COL, false);
   //
   // we need an extra col element for our buttons
   if (column == 0 && hasSelectionColumn) {
    colGroupElement.add(colElement);
    colElement.addAttribute(ElementNames.Attributes.WIDTH, "1");
    colElement = new Element(ElementNames.COL, false);
   }
   //
   // now test every column width
   if (width == EchoConstants.UNDEFINED_SIZE) {
    colElement.addAttribute(ElementNames.Attributes.WIDTH, "1*");
    allColumnsHaveWidths = false;
   } else {
    if (columnWidthsUsePercentUnits) {
     colElement.addAttribute(ElementNames.Attributes.WIDTH, width + "%");
    } else {
     colElement.addAttribute(ElementNames.Attributes.WIDTH, width);
    }
    someColumnsHaveWidths = true;
   }
   colGroupElement.add(colElement);
  }
  if (someColumnsHaveWidths) {
   tableElement.add(colGroupElement);
  }

  //----------------------------------------------------
  // Render the cells.  The Selectable table adds an
  // extra cell (TD) onto the right of the table.
  // If we have a header, then we have a "no-op" cell on
  // the right otherwise we have selectable cells
  // that can select a row.
  //----------------------------------------------------
  Element trElement, tdElement, theadElement, tbodyElement;
  Element divElement = null;
  
  theadElement = new Element("thead",true);
  tbodyElement = new Element("tbody",true);
  
  String rowHeight;
  for (int row = startRow; row < rowCount; ++row) {
   // Height calculation
   if (table.getRowHeight(row) == EchoConstants.UNDEFINED_SIZE) {
    allRowsHaveHeights = false;
    rowHeight = null;
   } else {
    if (rowHeightsUsePercentUnits) {
     rowHeight = table.getRowHeight(row) + "%";
    } else {
     rowHeight = Integer.toString(table.getRowHeight(row));
    }
   }

   //----------------------------------------------------
   // Table row creation
   //----------------------------------------------------
   boolean rowIsSelected = false;
   trElement = new Element(ElementNames.TR);
   trElement.addAttribute("id", id + "_" + row);
   if (hasHeader && row == startRow) {
    theadElement.add(trElement);
    if (isIE && hasScrollableRegions) {
     trElement.addAttribute("style","position:relative;top:expression(g_epst_synchTop('" + id + "',"+borderTableRequired+"));");
    }
   } else {
    tbodyElement.add(trElement);
    //
    // we are drawing visual row N but it could
    // represent model row X.  The selection model
    // tracks selection in the model rows.
    //
    if (selectableTable != null) {
     int underlyingIndex = getModelRowIndex(table, row);
     rowIsSelected = selectableTable.isSelectedIndex(underlyingIndex);
    }
    /*
    if (rowIsSelected) {
     trElement.addAttribute(ElementNames.Attributes.CLASS, selectionOnClass);
    } else {
     trElement.addAttribute(ElementNames.Attributes.CLASS, defaultClass);
    }
    */
    trElement.addAttribute(ElementNames.Attributes.CLASS, defaultClass);
   }

   //----------------------------------------------------
   // Now for each column in the row, render it
   //----------------------------------------------------
   Element tdSelectionHandle = null;
   for (int column = 0; column < columnCount; ++column) {
    //
    // we need an extra cell for the selection handles
    if (column == 0 && hasSelectionColumn) {
     tdElement = new Element(ElementNames.TD);
     trElement.add(tdElement);
     tdSelectionHandle = tdElement;
     tdElement.setWhitespaceRelevant(true);
     tdElement.addAttribute("align", "center");
     tdElement.addAttribute("valign", "middle");
     //
     // its a "no-op" cell if the current row is the header row
     // otherwise we need to add our selection handle code
     if (hasHeader && row == startRow) {
      tdElement.addAttribute(ElementNames.Attributes.CLASS, defaultClass);
     } else {
      Element selToggle = new Element(ElementNames.INPUT);
      selToggle.addAttribute(ElementNames.Attributes.CLASS, checkBoxClass);
      selToggle.setWhitespaceRelevant(true);
      selToggle.addAttribute("id", id + "_chk_" + row);
      if (multipleSelection)
       selToggle.addAttribute("type", "checkbox");
      else
       selToggle.addAttribute("type", "radio");
      selToggle.addAttribute("value", " ");
      if (rowIsSelected)
       selToggle.addAttribute("checked");
      if (! table.isEnabled())
       selToggle.addAttribute("disabled");
      if (allowSelection) {
       selToggle.addAttribute("onclick", _createSelectionScript(id, row, selectableTable));
      }
      tdElement.add(selToggle);
     }
    }
    //
    // now the rest of the cells 
    tdElement = new Element(ElementNames.TD);
    tdElement.setWhitespaceRelevant(true);
    tdElement.addAttribute("id", id + "_" + column + "_" + row);
    if (! table.isWrapAllowed())
     tdElement.addAttribute("nowrap");
    
    if (row >= startDataRow) {
     if (selectableTable != null && table.isEnabled() && allowSelection) {
      if (selectableTable.isRowClickSelection()) {
       //
       // if we use mouseup then we will beat any onclicks
       // handlers that may reside inside the TD, put there by other
       // components.  So we will select the row regardless
       tdElement.addAttribute("onmouseup",_createSelectionScript(id, row, selectableTable));
      }
     }
    }
        
    Coordinate coordinate = new Coordinate(column, row);
    Component cell = table.getComponent(coordinate);
    if (cell == null || ! cell.isVisible()) {
     tdElement.addAttribute(ElementNames.Attributes.CLASS, defaultClass);
    } else {
     ComponentPeer peer = tablePeer.getPeer(cell);
     ComponentStyle cellStyle = ComponentStyle.forComponent(peer);
     if (table.getBorderColor() != null && cell.getBackground() == null) {
      //
      // We now longer need the cell to have an explicit background
      // as we are setting it on the TR row.
      //
      //cellStyle.setBackground(defaultBackgroundClr);
     } else {
      cellStyle.setBackground(cell.getBackground());
     }
     /*
     // if the row is selected we want the background to
     // be the selection color regardless
     if (rowIsSelected) {
      cellStyle.setBackground(selectionColor);
     }
     */
     //
     // and back to the cells
     if (peer instanceof Alignment) {
      cellStyle.setHorizontalAlignment(((Alignment) peer).getHorizontalAlignment());
      cellStyle.setVerticalAlignment(((Alignment) peer).getVerticalAlignment());
     }
     cellStyle.addElementType(ElementNames.TD);
     String cellStyleName = rc.getDocument().addStyle(cellStyle);

     tdElement.addAttribute(ElementNames.Attributes.CLASS, cellStyleName);
     //
     // if we have a header and we are in that row then set the selection
     // handle cell to the same color as header column 0
     if (hasHeader && row == startRow && column == 0 && hasSelectionColumn)
      tdSelectionHandle.addAttribute(ElementNames.Attributes.CLASS, cellStyleName);

     peer.render(rc, tdElement);
     
     if (column == columnCount-1 && hasScrollableRegions && row > startRow) {
      tdElement.addHtml(columnPadding);
     }
    }

    int width = columnModel.getColumn(column).getWidth();
    if (width != EchoConstants.UNDEFINED_SIZE) {
     if (columnWidthsUsePercentUnits) {
      tdElement.addAttribute(ElementNames.Attributes.WIDTH, width + "%");
     } else {
      tdElement.addAttribute(ElementNames.Attributes.WIDTH, width);
     }
    }
    if (rowHeight != null) {
     tdElement.addAttribute(ElementNames.Attributes.HEIGHT, rowHeight);
    }

    trElement.add(tdElement);
   }
   //
   // rollover support
   if (allowRowRollovers && row >= 0) {
    String jsMsOut  = "g_epst_mouseout(this," +_qt(id)+","+row+");";
    String jsMsOver = "g_epst_mouseover(this,"+_qt(id)+","+row+");";
    
    trElement.addAttribute("onmouseover",jsMsOver);
    trElement.addAttribute("onmouseout",jsMsOut);
   }
   
   
  }
  tableElement.add(theadElement);
  tableElement.add(tbodyElement);

... you get the point.  Its very wordy, and as a developer you need to switch mindset between HTML and Java all the time. 

Now in its defence it also renders the most complex component in the EchoPoint library (with the exception of echopoint.Tree) and the component is very generic, being able to display any TableModel with sorting and selectability.

Introducing JspComponentPeer

The echopoint.ui.jsp.JspComponentPeer class allows you to write your component peers using JSP technology.  All you need to extend this abstract class and implement the abstract getJspPath() method.  This method tells the base peer class what JSP path to include as the content. 

That's it.  Nothing more to do.  Actually there is more. 

You will probably want to override the preRender() method to setup any nextapp.echoservlet.ComponentStyle objects and JavaScripts Services you require for your component.    For example :

protectedvoidpreRender(RenderingContext rc) {
    MyCustomComponent mcc = (MyCustomComponent) getComponent();
    addScriptInclude(rc,MCC_SERVICE_SCRIPT);
   
   
ComponentStyle style;
    style =
new
ComponentStyle();
    style.addElementType(
""
);
    style.setForeground(Color.RED);
    style.setBackground(Color.YELLOW);
    style.setFont(new Font(Font.VERDANA,Font.BOLD,16));
    putStyle("styleName1",style);

   style = newComponentStyle();
    style.addElementType(
""
);
    style.setForeground(Color.BLUE);
    style.setBackground(Color.PINK);
    style.setFont(
new
Font(Font.VERDANA,Font.ITALIC,20));
    putStyle(
"styleName2"
,style);
}

Once you have created ComponentStyle objects and "named" them via the putStyle() method you can access them in your JSP via the getStyle() and classEquals() methods.  For example :

...
<tr><td class="<%=peer.getStyle("styleName1")%>" >Some text in the JSP</td></tr>

<tr><td <%=peer.classEquals("styleName1")%> >Some more text in the JSP</td></tr>
....

(Can you pick the difference between the getStyle() and classEquals() methods from this example?)

Where does the variable "peer" come from I hear you ask.  Well I am glad you asked.  A reference to the current JspComponentPeer object is placed in the JSP request attribute space under the name JspComponentPeer.JSPCOMPONENTPEER.  Therefore the top of your component peer JSP page will always look something like this :

<%@ page language="java" %>
<%@ page import="echopoint.ui.jsp.JspComponentPeer" %>
<%@ page import="com.mycompany.components.MyCustomComponent" %>
<%
          JspComponentPeer peer = (JspComponentPeer) request.getAttribute(JspComponentPeer.JSPCOMPONENTPEER);
         MyCustomComponent mcc = (MyCustomComponent) peer.getComponent();
%>
...
...

One more thing you need to know about is how child components of your custom component are handled.  Well before the JSP page is run, all the child components of the JspComponentPeer's component are pre-rendered and made available to the JSP based peer.  You access them like this :

<td> <% peer.renderChild(out,mcc.getTopBitComponent()); %> </td>

What is happening here is that the HTML output of the child component is placed inside the JSP when this call is made.  Any styles required by the child component will have already been setup (during the pre-render) and hence the output will just work.

There is some more methods to help you when including nextapp.echo.ImageResource objects but I will leave this up to you to find out more.  Its fairly straight forward stuff. 

The real trick when using JspComponentPeer is to develop a JSP page that is truly dynamic.  For example :

What happens if the foreground color of your component changes?  This should be reflected in the resultant JSP some how. 

Typically you would override preRender(), add some ComponentStyle objects and then call getStyle() inside the JSP to dynamically set HTML class attributes! 

Perhaps a new JSP page should be used if the component changes state somehow? 

You should return the name of the JSP page to use in the getJspPath() method based on the state of your component!

You may have noticed that the standard nextapp.echoservlet.ComponentPeer.render() method has been made final.  This is because there are certain setup steps required to allow it to include the JSP page and these are done in this public render() method.  Therefore you now need to do your setup inside the new preRender() method.

You may have also noticed that JspComponentPeer is derived from echopoint.ui.util.EchoPointPeer.  This gives you a bunch of capabilities such as automatic property change and image update listeners, an ImageManager available and a stack of other code that ComponentPeers usually need.

What About JspTemplatePanel?

There is some overlap in the JspTemplatePanel component and this new JspComponentPeer class, not least the inclusion of JSP content. 

However the major difference is that JspComponentPeer can only be used as a UI peer class for a given component.  It does not use "JSP tags" to include content, nor does it allow dynamic EchoPoint style="xxx" information to be described in the JSP page.

Simply put, JspComponentPeer allows for a JSP to be rendered as the UI content of a component.

An Example - TimeZoneCalculator

This example code shows a simple component that has a not so simple rendering requirement.  It is a TimeZoneCalculator component that shows your current browser timezone and displays it relative to UTC.  It also shows other cities around the world.  Its is inspired by some HTML/JavaScript code I found years ago that is Copyright 1996 Ian Fennell.

You can finds this example in the EchoPoint Test WAR file and the source in the EchoPoint Test Project..

Translating this web thingy into nextapp.echoservlet.html.Element statements would have been prohibitive, however using the JSP approach saved me hours. 

Notice in TimeZoneCalculatorUI.java I used the following JSP path

 protected String getJspPath() {
       return "WEB-INF/classes/echopoint/quicktest/jsp/resources/TimeZoneCalculatorUI.jsp";
 }

This path lets me place the JSP file in the same directory structure as my Java class files.  The directory structure I used looks like this :

echopoint/quicktest/jsp/TimeZoneCalculator.java
echopoint/quicktest/jsp/TimeZoneCalculatorUI.java
echopoint/quicktest/jsp/TimeZoneCalculator.properties
echopoint/quicktest/jsp/resources/TimeZoneCalculatorUI.jsp
echopoint/quicktest/jsp/resources/TimeZoneCalculatorUI.js

When the code  is deployed to the application server, it ends up under WEB-INF/classes/.

Personally I like this structure from a source code management point of view and also being able to quickly find the file in an IDE like Eclipse is a bonus. 

This structure is in no way required.  The JSP path is processed according to the J2EE JSP documentation  :

http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/servlet/jsp/PageContext.html#include(java.lang.String)

Causes the resource specified to be processed as part of the current ServletRequest and ServletResponse being processed by the calling Thread. The output of the target resources processing of the request is written directly to the ServletResponse output stream.
...
If the relativeUrlPath begins with a "/" then the URL specified is calculated relative to the DOCROOT of the ServletContext for this JSP. If the path does not begin with a "/" then the URL specified is calculated relative to the URL of the request that was mapped to the calling JSP.

The TimeZoneCalculator output looks like this :

You will notice it doing neat tricks like keeping the time up to date.

The files that make up this component are listed below.  Notice how simple the TimeZoneCalculator.java and TimeZoneCalculatorUI.java files are.  All of the detail is in the TimeZoneCalculator.jsp and the TimeZoneCalculator.js files.

TimeZoneCalculator.java




package echopoint.quicktest.jsp;

/**
 * <code>TimeZoneCalculator</code> will display
 * the current local time, and then a series of other
 * time zone informatrion, relative to the local time.
 */
public class TimeZoneCalculator extends EchoPointComponent implements FieldSetter {

 public static final String COMPONENT_CHANGED = "TimeZoneCalculator";
 
 public static String[] STANDARD_CITIES = new String[] {
  "0%3",  "London",
  "1%3",   "Berlin",
  "-5%2",  "New York",
  "-8%2",  "Los Angeles",
  "9%0",   "Tokyo",
  "8%0",   "Hong Kong",
  "10%4",  "Sydney",
 };
 
 public static final String[] EXTRA_CITIES = new String[] {
  "4.30%0", "Afghanistan",
  "-3%0",  "Argentina",
  "9.30%4", "Australia - Adelaide",
  "10%0",  "Australia - Brisbane",
  "9.30%0", "Australia - Darwin",
  "10%4",  "Australia - Melbourne",
  "8%0",  "Australia - Perth",
  "10%4",  "Australia - Sydney",
  "10%5",  "Australia - Tasmania",
  "-4%0",  "Bolivia",
  "-5%1",  "Brazil - Andes",
  "-3%1",  "Brazil - East",
  "-4%1",  "Brazil - West",
  "6.30%0", "Burma (Myanmar)",
  "-7%2",  "Canada - Calgary",
  "-3.30%2", "Canada - Newfoundland",
  "-4%2",  "Canada - Nova Scotia",
  "-5%2",  "Canada - Quebec",
  "-5%2",  "Canada - Toronto",
  "-8%2",  "Canada - Vancouver",
  "-6%2",  "Canada - Winnipeg",
  "8%1",  "China - Mainland",
  "8%0",  "China - Taiwan",
  "-5%0",  "Colombia",
  "-5%1",  "Cuba",
  "2%1",  "Egypt",
  "2%3",  "Finland",
  "1%3",  "France",
  "1%3",  "Germany",
  "0%0",  "Ghana",
  "2%3",  "Greece",
  "5.30%0", "India",
  "8%0",  "Indonesia - Bali, Borneo",
  "9%0",  "Indonesia - Irian Jaya",
  "7%0",  "Indonesia - Sumatra, Java",
  "3.30%1", "Iran",
  "3%0",  "Iraq",
  "2%1",  "Israel",
  "-5%1",  "Jamaica",
  "3%0",  "Kenya",
  "9%0",  "Korea (North & South)",
  "8%0",  "Malaysia",
  "-6%1",  "Mexico City",
  "0%0",  "Morocco",
  "5.45%0", "Nepal",
  "12%6",  "New Zealand",
  "5%0",  "Pakistan",
  "-5%0",  "Peru",
  "8%0",  "Philippines",
  "1%3",  "Poland",
  "11%7",  "Russia - Kamchatka",
  "3%7",  "Russia - Moscow",
  "9%7",  "Russia - Vladivostok",
  "8%0",  "Singapore",
  "2%0",  "South Africa",
  "1%3",  "Spain",
  "1%3",  "Sweden",
  "7%0",  "Thailand",
  "12%0",  "Tonga",
  "2%3",  "Turkey",
  "3%1",  "Ukraine",
  "5%0",  "Uzbekistan",
  "7%0",  "Vietnam",
  "-9%2",  "USA - Alaska",
  "-5%2",  "USA - Atlanta",
  "-7%2",  "USA - Boulder",
  "-6%2",  "USA - Chicago",
  "-5%0",  "USA - East Indiana",
  "-10%0", "USA - Hawaii",
  "-8%2",  "USA - Seattle",
 };
 
 private List extraCities;  
 private List standardCities;
 
 private ImageReference worldImage;
 
 /**
  * Contructs a <code>TimeZoneCalculator</code>
  */
 public TimeZoneCalculator() {
  
  // construct our city lists
  standardCities = new ArrayList(STANDARD_CITIES.length);
  for (int i = 0; i < STANDARD_CITIES.length; i++) {
   standardCities.add(STANDARD_CITIES[i]);
  }  

  extraCities = new ArrayList(EXTRA_CITIES.length);
  for (int i = 0; i < EXTRA_CITIES.length; i++) {
   extraCities.add(EXTRA_CITIES[i]);
  }  
  setWorldImage(EchoPointIcons.icon32("world"));
 }
 
 /**
  * The TimeZoneData consists of a UTC offset and DST period in this format:
  * <p>
  * "UTCOffset%DST period' - where the % is a separator.
  * <p>
  * The UTC offset must be a value between 12 and -11 (a '+' is not needed for times ahead of UTC).
  * If the offset is not round hours, enter the minutes after a decimal point (full stop, period...)
  * e.g. India (5 hours 30 mins ahead of GMT) = 5.30
  * <p>
  * The DST period should be one of the following, which are 'built in':
  * <ul>
  * <li>0 - Daylight Savings never used, or have no info.
  * <li>1 - Uncertain; they do or have in the past, but don't have definite info.
  * <li>2 - DST from 1st Sun in April to Last Sun in October (USA/Canada)
  * <li>3 - DST from Last Sun in March to Last Sun in October (UK/Europe)
  * <li>4 - DST from Last Sun in October to Last Sun in March (Australia - NSW, Vic, ACT, SA)
  * <li>5 - DST from 1st Sun in October to Last Sun in March (Australia - Tasmania)
  * <li>6 - DST from 1st Sun in October to 1st Sun on/after 15 March (New Zealand)
  * <li>7 - DST from Last Sun in March to 1st Sun in September (Russia)
  * </ul>
  * <p>
  * Example: to add Italy the tmezone data would be represented as '1%3'
  * <br>
  * (standard time 1 hour ahead of GMT, Daylight Savings period 3 in list above)
  *
  */
 public void addStandardCity(String timeZoneData, String cityName) {
  standardCities.add(timeZoneData);
  standardCities.add(cityName);
  firePropertyChange(COMPONENT_CHANGED,null,null);
 }

 /**
  * @see TimeZoneCalculator#addStandardCity(String, String)
  */
 public void addExtraCity(String timeZoneData, String cityName) {
  extraCities.add(timeZoneData);
  extraCities.add(cityName);
  firePropertyChange(COMPONENT_CHANGED,null,null);
 }

 public String[] getExtraCities() {
  return (String[]) extraCities.toArray(new String[extraCities.size()]);
 }

 public String[] getStandardCities() {
  return (String[]) standardCities.toArray(new String[standardCities.size()]);
 }

 public ImageReference getWorldImage() {
  return worldImage;
 }

 public void setWorldImage(ImageReference reference) {
  set(this,"worldImage",reference);
 }

 public Object set(Field f, Object newValue) throws Exception {
  Object oldValue = f.get(this);
  f.set(this,newValue);
  return oldValue;
 }
}

TimeZoneCalculatorUI.java


package echopoint.quicktest.jsp;

import nextapp.echoservlet.EchoServer;
import nextapp.echoservlet.HtmlDocument;
import nextapp.echoservlet.RenderingContext;
import nextapp.echoservlet.Service;
import nextapp.echoservlet.StaticText;
import nextapp.echoservlet.html.Element;
import nextapp.echoservlet.html.ElementNames;

import echopoint.ui.jsp.JspComponentPeer;

/**
 * <code>TimeZoneCalculatorUI</code>
 */
public class TimeZoneCalculatorUI extends JspComponentPeer {

 public static final Service TZC_SERVICE_SCRIPT =
  StaticText.createFromResource("EP_TZC", "/echopoint/quicktest/jsp/resources/TimeZoneCalculatorUI.js");
  
 static {
  EchoServer.addGlobalService(TZC_SERVICE_SCRIPT);
 }

 /**
  * @see echopoint.ui.jsp.JspComponentPeer#getJspPath()
  */
 protected String getJspPath() {
  return "WEB-INF/classes/echopoint/quicktest/jsp/resources/TimeZoneCalculatorUI.jsp";
 }

 /**
  * @see echopoint.ui.util.EchoPointComponentPeer#registered()
  */
 public void registered() {
  super.registered();
  trackImage("worldImage");
 }

 /**
  * @see echopoint.ui.jsp.JspComponentPeer#setupJspRender(nextapp.echoservlet.RenderingContext)
  */
 protected void preRender(RenderingContext rc) {
  
  TimeZoneCalculator tzc = (TimeZoneCalculator) getComponent();
  addScriptInclude(rc,TZC_SERVICE_SCRIPT);
  
  
  String htmlId = getId().toString();
  StringBuffer sbScript = new StringBuffer();
  sbScript.append("var eptzc");
  sbScript.append(htmlId);
  sbScript.append("= new EPTZC('");
  sbScript.append(htmlId);
  sbScript.append("');\n");
  
  String[] standardCities = tzc.getStandardCities();
  for (int i = 0; i < standardCities.length; i +=2) {
   sbScript.append("eptzc");
   sbScript.append(htmlId);
   sbScript.append(".addStandardCity('");
   sbScript.append(standardCities[i]);
   sbScript.append("','");
   sbScript.append(standardCities[i+1]);
   sbScript.append("');\n");
  }  
  String[] extraCities = tzc.getExtraCities();
  for (int i = 0; i < extraCities.length; i +=2) {
   sbScript.append("eptzc");
   sbScript.append(htmlId);
   sbScript.append(".addExtraCity('");
   sbScript.append(extraCities[i]);
   sbScript.append("','");
   sbScript.append(extraCities[i+1]);
   sbScript.append("');\n");
  }  

  Element script = new Element(ElementNames.SCRIPT);
  addOnce(rc.getDocument().getHeadElement(),script,getId());
  script.addHtml(sbScript.toString());
  rc.getDocument().addScript("eptzc" + htmlId + ".startClocks()",HtmlDocument.EVENT_ONLOAD);
 }

 public static void register() {
  EchoServer.loadPeerBindings("echopoint.quicktest.jsp.TimeZoneCalculator");
 }

}

TimeZoneCalculatorUI.jsp




<%@ page language="java" %>
<%@ page import="echopoint.ui.jsp.JspComponentPeer" %>
<%@ page import="echopoint.quicktest.jsp.*" %>

<%
 TimeZoneCalculatorUI peer = (TimeZoneCalculator) request.getAttribute(JspComponentPeer.JSPCOMPONENTPEER);
 TimeZoneCalculator tzc = (TimeZoneCalculator) peer.getComponent();
 String htmlId = peer.getId().toString();
%>

<style>

.cityNamesSelect {
 font-family:'tahoma';
 background : #DABA50; 
 font-weight:bold;
 color:#0000ff;
 text-align:right;
}

.cityNames {
 font-weight:bold;
 color:#0000ff;
 text-align:right;
 padding : 0 5 0 0;
}


.headings {
 font-weight:bold;
 text-align:center;
 padding : 0 15 0 15;
}


.infoCell {
 text-align:center;
 background : #ffffff;
}
</style>

<center>
<table class="default" cellspacing=2 cellpadding=0 border=0>
 <% String fieldID = ""; %>

 <tr>
 <td>&nbsp;</td>
 <td class="headings" width="20%">Time</TD>
 <TD class="headings">Relative to Local</TD>
 <TD class="headings">Relative to UTC</TD>
 <TD class="headings">Daylight Savings?</TD>
 </TR>

 <% fieldID = htmlId + "localTime"; %>
 <tr>
 <td class="cityNames">Local Time</td>
 <td class="infoCell" id="<%=fieldID%>"></td>
 <TD class="infoCell" ID="<%=fieldID+"RPT1" %>"></TD>
 <TD class="infoCell" ID="<%=fieldID+"RPT2" %>"></TD>
 <TD class="infoCell" ID="<%=fieldID+"RPT3" %>"></TD>
 </tr>

 <% fieldID = htmlId + "gmtTime"; %>
 <td class="cityNames">UTC</td>
 <td class="infoCell" id="<%=fieldID%>"></td>
 <TD class="infoCell" ID="<%=fieldID+"RPT1" %>"></TD>
 <TD class="infoCell" ID="<%=fieldID+"RPT2" %>"></TD>
 <TD class="infoCell" ID="<%=fieldID+"RPT3" %>"></TD>
 </tr>

 <tr><td colspan="5" class="cityNames">&nbsp;</td></tr>

 <!-- standard city list -->
 <% for (int i = 0; i < standardCities.length; i +=2) {
  fieldID = htmlId + "standardCity" + i;
 %>
  <TR>
  <TD class="cityNames"><%=standardCities[i+1]%></TD>
  <TD class="infoCell" ID="<%=fieldID%>"></TD>
  <TD class="infoCell" ID="<%=fieldID+"RPT1" %>"></TD>
  <TD class="infoCell" ID="<%=fieldID+"RPT2" %>"></TD>
  <TD class="infoCell" ID="<%=fieldID+"RPT3" %>"></TD>
  </TR>
 <% }; //for %>
 
 <!-- extra city list -->
 <tr>
 <td class="cityNames">
 <select class="cityNamesSelect" onchange="eptzcupdateExtraCity('<%=htmlId%>',this)" > 
 <% for (int i = 0; i < extraCities.length; i +=2) { %>
  <option class="cityNamesSelect" value="<%=extraCities[i]%>"><%=extraCities[i+1]%>&nbsp;</option>');
 <% }; //for %>
 </select></td>
 <TD class="infoCell" ID="<%=htmlId + "extraCity" %>"></TD>
 <TD class="infoCell" ID="<%=htmlId + "extraCityRPT1" %>"></TD>
 <TD class="infoCell" ID="<%=htmlId + "extraCityRPT2" %>"></TD>
 <TD class="infoCell" ID="<%=htmlId + "extraCityRPT3" %>"></TD>
 </tr>
</table></center>

TimeZoneCalculatorUI.js


//
// This component is based on the following work:
//
//=========== JavaScript International Time Calculator ===========
//=========== Copyright 1996 Ian Fennell (
redlodge@gem.co.za) all rights reserved =========
//=========== written from scratch Oct '96 ===========
//
//======YOU MAY COPY, USE AND MODIFY THE CONTENTS OF THIS FILE ON CONDITION THAT: =========
//====== You include the top 2 lines of this comment.
//====== If it is posted on the WWW you let me know where it is being used.
//====== You are not a registered company or commercial organisation.
//
//=========== Customizing this script ==========
// It's easy to change the locations displayed in the table or add new items to the form listbox.
// You can have as many places as you like listed in each.
// To add a location you need to know its standard offset from GMT (aka Universal Co-ordinated Time)
// If you know that it observes Daylight Savings Time that's useful, but not essential.
//
// The GMT offset and DST period are held in this format:
// 'GMT offset%DST period' - the % is a separator.
//
//The GMT offset must be a value between 12 and -11 (a '+' is not needed for times ahead of GMT).
//If the offset is not round hours, enter the minutes after a decimal point (full stop, period...)
// e.g. India (5 hours 30 mins ahead of GMT) = 5.30
//
//The DST period should be one of the following, which are 'built into' this page:
// 0 - Daylight Savings never used, or have no info.
// 1 - Uncertain; they do or have in the past, but don't have definite info.
// 2 - DST from 1st Sun in April to Last Sun in October (USA/Canada)
// 3 - DST from Last Sun in March to Last Sun in October (UK/Europe)
// 4 - DST from Last Sun in October to Last Sun in March (Australia - NSW, Vic, ACT, SA)
// 5 - DST from 1st Sun in October to Last Sun in March (Australia - Tasmania)
// 6 - DST from 1st Sun in October to 1st Sun on/after 15 March (New Zealand)
// 7 - DST from Last Sun in March to 1st Sun in September (Russia)
//(If you are sure of other DST periods and know JavaScript you can go to the function 'CheckDST' and add it -
//alternatively you can mail me so that I can update the page).
//
//Example: to add Italy the data would be stored as '1%3'
// (standard time 1 hour ahead of GMT, Daylight Savings period 3 in list above)
// to add Italy to the table, open this file in any text editor, go to CUSTOMIZE TABLE below and add the line -
// '1%3', 'Italy',
//to add Italy to the form list box, go to CUSTOMIZE FORM below and add the line -
// <OPTION VALUE= '1%3'>Italy
//Not too complicated, hey? And of course you can delete any places you don't much care for.

var EPTZCMap = new Array();
var newline= (navigator.appVersion.lastIndexOf('Win') != -1) ? '\r\n' : '\n';

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Constructor
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function EPTZC(htmlId) {
 this.htmlId = htmlId;
 this.dstZones = new Array ();
 this.timeReport = new Array ();
 this.tableTimes = new Array();
 this.currentTZQuery = null;

 this.gmtOffset = new Date().getTimezoneOffset();

 this.timerID = null;
 this.timerRunning = false;

 this.standardCities = new Array();
 this.extraCities = new Array();
 // the code seems to work in reverse as far as GMT offset is concerned
 this.gmtOffset = - (this.gmtOffset);
 
 this.checkDST();
 
 EPTZCMap[htmlId] = this;
}

EPTZC.prototype.addStandardCity = function(timeZoneData,cityName) {
 this.standardCities[this.standardCities.length] = timeZoneData;
 this.standardCities[this.standardCities.length] = cityName;
}

EPTZC.prototype.addExtraCity = function(timeZoneData,cityName) {
 this.extraCities[this.extraCities.length] = timeZoneData;
 this.extraCities[this.extraCities.length] = cityName;
}

//--------------------------------------
// Called to find an HTML element with our htmlId prefix
//--------------------------------------
EPTZC.prototype.getE = function(extraHtmlId) {
   var e = document.getElementById(this.htmlId + extraHtmlId);
   if (e == null)
 alert('TZC error - Element ' + this.htmlId + extraHtmlId + ' could not be found');
   return e; 
}

EPTZC.prototype.Into24hrs = function(time) {
 if ( time > 1440)
  { time -= 1440}
 else
  { if ( time <0) { time = 1440 + time } };
 return time;
} // --- Into24hrs

EPTZC.prototype.GMTnow = function(GMT) {
 var time = new Date();
 hrs = time.getHours();
 mins = time.getMinutes();
 GMT = (hrs*60 + mins) - this.gmtOffset;
 GMT = this.Into24hrs(GMT);
 return GMT;
}

EPTZC.prototype.formatRelative = function(time) {
 var Report = null;
 var Direction  = (time > 0) ? ' ahead' : ' behind';
 time = Math.abs (time);
 var Hours = Math.floor (time/60);
 var Mins = (time - Hours * 60);
 Report = Hours + 'h ';
 if (Mins != 0) {Report=Report+Mins + 'm '};
 Report = Report+ Direction;
 if (time== 0) {Report='Same Time'};
 return Report;
};

EPTZC.prototype.formatTime = function(time) {
 var fHours = Math.floor (time/60) ;
 if (fHours <= 9) {fHours = '0' + fHours};
 var fMins = time - (fHours * 60);
 if (fMins <= 9) {fMins = '0' + fMins};
 var fTime = fHours + ':' + fMins;
 return fTime;
};

EPTZC.prototype.checkDST = function() {
 var uNow = new Date();
 var uMonth = uNow.getMonth();
 var uDate = uNow.getDate();
 var uDay = uNow.getDay();

 var FirstSun8Feb = ( (uMonth == 1 && uDate > 14) || uMonth > 1 ) ? true : false;
 if ((uMonth == 1)&&(uDate >= 8)) {
  var DaysLeft = 14  - uDate;
  FirstSun8Feb = (uDay + DaysLeft <= 6) ? true : false;
 };

 var FirstSun15Mar = ( (uMonth == 2 && uDate > 21) || uMonth > 2 ) ? true : false;
 if ((uMonth == 2)&&(uDate >= 15)) {
  DaysLeft = 21  - uDate;
  FirstSun15Mar = (uDay + DaysLeft <= 6) ? true : false;
 };

 var LastSunMar = (uMonth > 2) ? true : false;
 if ((uMonth == 2)&&(uDate >= 25)) {
  DaysLeft = 31  - uDate;
  LastSunMar = (uDay + DaysLeft <= 6) ? true : false;
 };

 var FirstSunApr = ( (uMonth == 3 && uDate > 7) || uMonth > 3 ) ? true : false;
 if ((uMonth == 3)&&(uDate <= 7)) {
  var DaysGone = 7  - uDate;
  FirstSunApr = (uDay - DaysGone >0) ? true : false;
 };

 var LastSunSep = (uMonth > 8) ? true : false;
 if ((uMonth == 8)&&(uDate >= 24)) {
  DaysLeft = 30  - uDate;
  LastSunSep = (Day + DaysLeft <= 6) ? true : false;
 };

 var FirstSunOct = ( (uMonth == 9 && uDate > 7) || uMonth > 9  ) ? true : false;
 if (uMonth == 9 && uDate <= 7) {
  DaysGone = 7  - uDate;
  FirstSunOct = (uDay - DaysGone >0) ? true : false;
 };

 var FirstSun15Oct = ( (uMonth == 9)&&(uDate > 21) || (uMonth > 9) ) ? true : false;
 if ( uMonth == 9 && (uDate >= 15 || uDate <= 21) ) {
  DaysLeft = 21  - uDate;
  FirstSun15Oct = (uDay + DaysLeft <= 6) ? true : false;
 };

 var LastSunOct = (uMonth > 9) ? true : false;
 if ((uMonth == 9)&&(uDate >= 25)) {
  DaysLeft = 31  - uDate;
  LastSunOct = (uDay + DaysLeft <= 6) ? true : false;
 };

 this.dstZones[0] = 'X';
 this.dstZones[1] = '?';
 this.dstZones[2] =  (FirstSunApr && !LastSunOct) ? 'Y' : 'N'; //usa/canada
 this.dstZones[3] =  (LastSunMar && !LastSunOct) ? 'Y' : 'N'; //uk/europe
 this.dstZones[4] =  (LastSunOct || !LastSunMar) ? 'Y' : 'N'; //aus 
 this.dstZones[5] =  (FirstSunOct || !LastSunMar) ? 'Y' : 'N'; //aus-tasmania
 this.dstZones[6] =  (FirstSunOct || !FirstSun15Mar) ? 'Y' : 'N';//nz
 this.dstZones[7] =  (LastSunMar && !LastSunSep) ? 'Y' : 'N'; //russia
}


EPTZC.prototype.zoneDataHandler = function(ZoneData) {
 this.timeReport[0] = ''; this.timeReport[1] = ''; this.timeReport[2] = ''; this.timeReport[3] = ''; 

 var qGMTparse = parseFloat(ZoneData);
 var qGMToffset_hrs = parseInt(qGMTparse, 10) ;
 var qGMToffset_min= parseInt ( Math.round((qGMTparse-qGMToffset_hrs) * 100), 10);
 var qDSTperiod = ZoneData.charAt (ZoneData.length - 1);
 var qGMTperiod = 1440/60;
 if ( (qGMToffset_hrs > 12) || (qGMToffset_hrs <-11) ) {this.timeReport[0] = 'BAD DATA'; return};
 if (qDSTperiod > this.dstZones.length) {this.timeReport[3] = 'BAD DATA'};


 var relGMT = (qGMToffset_hrs * 60) + qGMToffset_min;
 if (this.dstZones [qDSTperiod] == 'Y') {relGMT += 60; this.timeReport[3] = '(+1 hour)'};
 if (this.dstZones [qDSTperiod] == 'N') {this.timeReport[3] = 'None'};
 if (this.dstZones [qDSTperiod] == 'X') {this.timeReport[3] = 'N/A'};
 if (this.dstZones [qDSTperiod] == '?') {this.timeReport[3] = 'Uncertain'};

 var qPlaceTotMins = this.GMTnow (qPlaceTotMins);
 qPlaceTotMins += relGMT;
 qPlaceTotMins = this.Into24hrs (qPlaceTotMins);
 this.timeReport[0] = qPlaceTotMins;

 this.timeReport[2] = this.formatRelative (relGMT);

 var relLoc = relGMT - this.gmtOffset;
 this.timeReport[1] = this.formatRelative (relLoc);
}

EPTZC.prototype.updateExtraCity = function(ZoneData) {
 this.currentTZQuery = ZoneData;
 this.zoneDataHandler(ZoneData);
 //
 // updates text field bit with time info.
 this.getE('extraCity').innerHTML = this.formatTime ( this.timeReport[0] );
 this.getE('extraCityRPT1').innerHTML = this.timeReport[1];
 this.getE('extraCityRPT2').innerHTML = this.timeReport[2];
 this.getE('extraCityRPT3').innerHTML = this.timeReport[3];
}


EPTZC.prototype.updateStandardCities = function() {
 
 for (var idx = 0; idx < this.standardCities.length; idx+=2) {
  this.zoneDataHandler( this.standardCities[idx] );
  
  this.getE('standardCity'+idx).innerHTML = this.formatTime ( this.timeReport[0] );
  this.getE('standardCity'+idx+'RPT1').innerHTML = this.timeReport[1];
  this.getE('standardCity'+idx+'RPT2').innerHTML = this.timeReport[2];
  this.getE('standardCity'+idx+'RPT3').innerHTML = this.timeReport[3];
  
 }; //for
}


EPTZC.prototype.stopclocks = function(){
 if(this.timerRunning)
  clearTimeout(this.timerID);
 this.timerRunning = false;
}

EPTZC.prototype.showclocks = function() {
 var GMT = this.GMTnow(GMT);
 var fTime;
 var tzData;
 
 fTime = this.formatTime(this.Into24hrs(GMT + this.gmtOffset));
 tzData = "" + this.gmtOffset/60 + "%1";
 this.zoneDataHandler(tzData);
 this.getE('localTime').innerHTML = this.formatTime ( this.timeReport[0] );
 this.getE('localTimeRPT1').innerHTML = this.timeReport[1];
 this.getE('localTimeRPT2').innerHTML = this.timeReport[2];
 this.getE('localTimeRPT3').innerHTML = this.timeReport[3];
 
 fTime = this.formatTime(this.Into24hrs(GMT) );
 tzData = "0%0";
 this.zoneDataHandler(tzData);
 this.getE('gmtTime').innerHTML = fTime;
 this.getE('gmtTimeRPT1').innerHTML = this.timeReport[1];
 this.getE('gmtTimeRPT2').innerHTML = this.timeReport[2];
 this.getE('gmtTimeRPT3').innerHTML = this.timeReport[3];

 this.updateStandardCities();

 if (this.currentTZQuery != null) {
  this.updateExtraCity(this.currentTZQuery)
 };
 this.timerID = setTimeout('eptzcShowClocks(\'' + this.htmlId + '\')', 10000);
 this.timerRunning = true;
}

EPTZC.prototype.startClocks = function() {
 this.stopclocks();
 this.updateStandardCities();
 if (this.extraCities.length > 0)
  this.updateExtraCity(this.extraCities[0]);
 this.showclocks();
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Called when the extra cities select field changes
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function eptzcupdateExtraCity(htmlId,selectField) {
 var tzc = eptzcOBJ(htmlId);
 tzc.updateExtraCity(selectField.options[selectField.selectedIndex].value);
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Called to a timer
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function eptzcShowClocks(htmlId) {
 var tzc = eptzcOBJ(htmlId);
 tzc.showclocks();
}

//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Finds EPTZC objects
//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
function eptzcOBJ(htmlId) {
 var tzc = EPTZCMap[htmlId];
 if (tzc == null)
  alert('ASSERT : EPTZC TimeZone Calculator error : object ' + htmlId + ' not found');
 return tzc;
}

 


Home

SourceForge Logo

The EchoPoint project is kindly hosted by SourceForge