Monday, March 1, 2010

Notes about Groovy's DataSet

I have blogged before about GroovySql and I find that it meets my needs nicely. However, some people prefer to use Groovy's DataSet class which the Groovy 1.7.1 API documentation describes as "an enhancement of Groovy's Sql class providing support for accessing and querying databases using POGO fields and operators rather than JDBC-level API calls and RDBMS column names."

Before getting to the DataSet example, I'll quickly demonstrate using GroovySql used to get the IT (department 60) employees from the HR sample schema in most Oracle databases.


#!/usr/bin/env groovy

// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

import groovy.sql.Sql
sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:orcl", "hr", "hr",
"oracle.jdbc.pool.OracleDataSource")
sql.eachRow("SELECT employee_id, last_name, first_name from employees where department_id = 60")
{
println "Employee of Department 200 is ${it.first_name} ${it.last_name}."
}


The IT's department number is hard-coded into the query, but it works for this simple example and the output lists five employees in the IT department.


Employee of Department 60 is Alexander Hunold.
Employee of Department 60 is Bruce Ernst.
Employee of Department 60 is David Austin.
Employee of Department 60 is Valli Pataballa.
Employee of Department 60 is Diana Lorentz.


This same result can be achieved with the Groovy DataSet as shown next.


#!/usr/bin/env groovy

// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

import groovy.sql.DataSet
import groovy.sql.Sql
sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:orcl", "hr", "hr",
"oracle.jdbc.pool.OracleDataSource")
def employee = new DataSet(sql, 'EMPLOYEES')
def itEmployees = employee.findAll { it.department_id = 60 }
itEmployees.each{ println "Employee of Department 60 is ${it.first_name} ${it.last_name}" }


Comparing the second example (using DataSet) to the first example (using GroovySql directly) shows that the biggest difference is between using SQL/JDBC statements with GroovySql and using a more Java/Groovy-like syntax with the DataSet approach.

Now, let's replace the hard-coded 60 with an ID passed into the script as a command-line argument. Here is the version using GroovySql, followed by its output.


#!/usr/bin/env groovy

// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

if (!args) { println "Enter a Department ID!"; System.exit(-1); }

import groovy.sql.DataSet
import groovy.sql.Sql
departmentId = args[0]
sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:orcl", "hr", "hr",
"oracle.jdbc.pool.OracleDataSource")
sql.eachRow("SELECT employee_id, last_name, first_name from employees where department_id = ${departmentId}")
{
println "Employee of Department ${departmentId} is ${it.first_name} ${it.last_name}."
}




A first cut at using DataSet might look like this:


#!/usr/bin/env groovy

// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

if (!args) {println "Enter a Department ID!"; System.exit(-1);}

import groovy.sql.DataSet
import groovy.sql.Sql
departmentId = args[0]
def employee = new DataSet(sql, 'EMPLOYEES')
def itEmployees = employee.findAll { it.department_id = departmentId }
itEmployees.each{ println "Employee of Department ${departmentId} is ${it.first_name} ${it.last_name}" }


This doesn't work so well, as shown in the next screen snapshot:



The error message shown in the above screen snapshot states "Caught: groovy.lang.MissingPropertyException: No such property: sql for class: ..." In an effort to deal with this, I add static typing to the department ID variable as demonstrated in the next code listing.


#!/usr/bin/env groovy

if (!args) {println "Enter a Department ID!"; System.exit(-1);}

// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

import groovy.sql.DataSet
import groovy.sql.Sql
int departmentId = args[0]
//sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:orcl", "hr", "hr",
// "oracle.jdbc.pool.OracleDataSource")
//sql.eachRow("SELECT employee_id, last_name, first_name from employees where department_id = ${departmentId}")
//{
// println "Employee of Department ${departmentId} is ${it.first_name} ${it.last_name}."
//}
def employee = new DataSet(sql, 'EMPLOYEES')
def itEmployees = employee.findAll { it.department_id = departmentId }
itEmployees.each{ println "Employee of Department 60 is ${it.first_name} ${it.last_name}" }


Unfortunately, there is still an error as shown in the next screen snapshot.



The error is simply a result of not casting the String argument to an integer. The next code listing addresses that.


#!/usr/bin/env groovy
// Need to include the DataSource class on the classpath. Might look like this:
//
// groovy -cp C:\app\Dustin\product\11.1.0\db_1\jdbc\lib\ojdbc6.jar getEmployeesViaDataSet.groovy

if (!args) {println "Enter a Department ID!"; System.exit(-1);}

import groovy.sql.DataSet
import groovy.sql.Sql
int departmentId = args[0] as Integer
sql = Sql.newInstance("jdbc:oracle:thin:@localhost:1521:orcl", "hr", "hr",
"oracle.jdbc.pool.OracleDataSource")
def employee = new DataSet(sql, 'EMPLOYEES')
def itEmployees = employee.findAll { it.department_id = departmentId }
itEmployees.each{ println "Employee of Department ${departmentId} is ${it.first_name} ${it.last_name}" }


The output from trying to run this version is shown next. In this case, the error message states: "java.sql.SQLSyntaxErrorException: ORA-00936: missing expression." The departmentId variable is obviously not working in the closure.



One of the limitations of the Groovy DataSet is that constants (numbers and literal strings) must be used in construction of the narrowing clauses. I often need my SQL/JDBC statements to be dynamically populated and in such cases the GroovySql is the better choice. Although Groovy DataSet supports inserts in addition to queries (see its add method), the DataSet does not support updates or deletes.

There is one final disadvantage of DataSet that is fairly well advertised. Code using the DataSet must be included on the classpath whether it is a script or a class. Up until now, I've run the script from the same directory in which the script resides, so it was automatically on the classpath. The next screen snapshot indicates what happens when I run the script from a different directory.



The error states: "groovy.lang.GroovyRuntimeException: Could not find the ClassNode for MetaClass: org.codehaus.groovy.runtime.metaclass.ClosureMetaClass@1a0b53e[class getEmployeesViaDataSet$_run_closure1]". This error is documented in bug GROOVY-2450, bug GROOVY-1877, and in Groovy Goodness: Groovy SQL Dataset. Perhaps most obviously, even the API documentation for DataSet warns: "Currently, the Groovy source code for any accessed POGO must be on the classpath at runtime." The warning applies to code run as script or as a class.

The next screen snapshot demonstrates that including the directory in which the script presides directly on the classpath takes care of the previously encountered "Could not find ClassNode for MetaClass" error.




One of the interesting features of the DataSet is that it provides a getSql() method and a getParameters() method that make it easy to see the SQL statement underlying a DataSet along with the parameters it uses. Similarly the rows() method returns the result rows from a query.


Conclusion

Groovy's DataSet provides some nice advantages such as a more object friendly syntax for those who are SQL-adverse and providing easy access to the SQL that underlies it. However, the DataSet also has some disadvantages from the small nuisance of needing to include the code using DataSet on the classpath to more significant drawbacks of not being able to use it for updates/deletes and not being able to use dynamic values to narrow searches. I find GroovySql easy enough to use and appreciate its consistency in satisfying different needs. Therefore, I generally prefer GroovySql, but I also understand that others may find DataSet's advantages make it more attractive.

Wednesday, February 24, 2010

Groovier Java TimeZone ID Handling

Many Java developers are not fond of Java's standard APIs for handling Dates and Calendars. In fact, it is difficult to think of examples from the Java standard APIs that better illustrate the mistake of allowing ultimate and mostly unused flexibility to result in an unnecessarily high degree of pain for the vast majority of users of the API. One of the things a Java developer using Calendar may need to do is specify a particular TimeZone. The specification of a TimeZone is done via String-based TimeZone IDs. This blog post demonstrates getting these String-based IDs with both Java and Groovy.

The TimeZone class provides a static method for getting an array of the Strings representing recognized TimeZone IDs. The availability of this method, TimeZone.getAvailableIDs(), allows a developer to programmatically access TimeZone IDs supported by the current JRE release or via Timezone Updater. I have blogged previously on the value of using simple Java tests. The next code listing is an example of just that and provides the available set of TimeZone IDs for a given environment.

TimeZoneIds.java

package dustin.examples;

import java.util.Arrays;
import java.util.TimeZone;
import static java.lang.System.out;

public class TimeZoneIds
{
public static void main(final String[] arguments)
{
out.println(Arrays.toString(TimeZone.getAvailableIDs()));
}
}


Compiling and running the above class results in output like the following:


[Etc/GMT+12, Etc/GMT+11, MIT, Pacific/Apia, Pacific/Midway, Pacific/Niue, Pacific/Pago_Pago, Pacific/Samoa, US/Samoa, America/Adak, America/Atka, Etc/GMT+10, HST, Pacific/Fakaofo, Pacific/Honolulu, Pacific/Johnston, Pacific/Rarotonga, Pacific/Tahiti, SystemV/HST10, US/Aleutian, US/Hawaii, Pacific/Marquesas, AST, America/Anchorage, America/Juneau, America/Nome, America/Yakutat, Etc/GMT+9, Pacific/Gambier, SystemV/YST9, SystemV/YST9YDT, US/Alaska, America/Dawson, America/Ensenada, America/Los_Angeles, America/Tijuana, America/Vancouver, America/Whitehorse, Canada/Pacific, Canada/Yukon, Etc/GMT+8, Mexico/BajaNorte, PST, PST8PDT, Pacific/Pitcairn, SystemV/PST8, SystemV/PST8PDT, US/Pacific, US/Pacific-New, America/Boise, America/Cambridge_Bay, America/Chihuahua, America/Dawson_Creek, America/Denver, America/Edmonton, America/Hermosillo, America/Inuvik, America/Mazatlan, America/Phoenix, America/Shiprock, America/Yellowknife, Canada/Mountain, Etc/GMT+7, MST, MST7MDT, Mexico/BajaSur, Navajo, PNT, SystemV/MST7, SystemV/MST7MDT, US/Arizona, US/Mountain, America/Belize, America/Cancun, America/Chicago, America/Costa_Rica, America/El_Salvador, America/Guatemala, America/Indiana/Knox, America/Indiana/Tell_City, America/Knox_IN, America/Managua, America/Menominee, America/Merida, America/Mexico_City, America/Monterrey, America/North_Dakota/Center, America/North_Dakota/New_Salem, America/Rainy_River, America/Rankin_Inlet, America/Regina, America/Swift_Current, America/Tegucigalpa, America/Winnipeg, CST, CST6CDT, Canada/Central, Canada/East-Saskatchewan, Canada/Saskatchewan, Chile/EasterIsland, Etc/GMT+6, Mexico/General, Pacific/Easter, Pacific/Galapagos, SystemV/CST6, SystemV/CST6CDT, US/Central, US/Indiana-Starke, America/Atikokan, America/Bogota, America/Cayman, America/Coral_Harbour, America/Detroit, America/Fort_Wayne, America/Grand_Turk, America/Guayaquil, America/Havana, America/Indiana/Indianapolis, America/Indiana/Marengo, America/Indiana/Petersburg, America/Indiana/Vevay, America/Indiana/Vincennes, America/Indiana/Winamac, America/Indianapolis, America/Iqaluit, America/Jamaica, America/Kentucky/Louisville, America/Kentucky/Monticello, America/Lima, America/Louisville, America/Montreal, America/Nassau, America/New_York, America/Nipigon, America/Panama, America/Pangnirtung, America/Port-au-Prince, America/Resolute, America/Thunder_Bay, America/Toronto, Canada/Eastern, Cuba, EST, EST5EDT, Etc/GMT+5, IET, Jamaica, SystemV/EST5, SystemV/EST5EDT, US/East-Indiana, US/Eastern, US/Michigan, America/Caracas, America/Anguilla, America/Antigua, America/Argentina/San_Luis, America/Aruba, America/Asuncion, America/Barbados, America/Blanc-Sablon, America/Boa_Vista, America/Campo_Grande, America/Cuiaba, America/Curacao, America/Dominica, America/Eirunepe, America/Glace_Bay, America/Goose_Bay, America/Grenada, America/Guadeloupe, America/Guyana, America/Halifax, America/La_Paz, America/Manaus, America/Marigot, America/Martinique, America/Moncton, America/Montserrat, America/Port_of_Spain, America/Porto_Acre, America/Porto_Velho, America/Puerto_Rico, America/Rio_Branco, America/Santiago, America/Santo_Domingo, America/St_Barthelemy, America/St_Kitts, America/St_Lucia, America/St_Thomas, America/St_Vincent, America/Thule, America/Tortola, America/Virgin, Antarctica/Palmer, Atlantic/Bermuda, Atlantic/Stanley, Brazil/Acre, Brazil/West, Canada/Atlantic, Chile/Continental, Etc/GMT+4, PRT, SystemV/AST4, SystemV/AST4ADT, America/St_Johns, CNT, Canada/Newfoundland, AGT, America/Araguaina, America/Argentina/Buenos_Aires, America/Argentina/Catamarca, America/Argentina/ComodRivadavia, America/Argentina/Cordoba, America/Argentina/Jujuy, America/Argentina/La_Rioja, America/Argentina/Mendoza, America/Argentina/Rio_Gallegos, America/Argentina/Salta, America/Argentina/San_Juan, America/Argentina/Tucuman, America/Argentina/Ushuaia, America/Bahia, America/Belem, America/Buenos_Aires, America/Catamarca, America/Cayenne, America/Cordoba, America/Fortaleza, America/Godthab, America/Jujuy, America/Maceio, America/Mendoza, America/Miquelon, America/Montevideo, America/Paramaribo, America/Recife, America/Rosario, America/Santarem, America/Sao_Paulo, Antarctica/Rothera, BET, Brazil/East, Etc/GMT+3, America/Noronha, Atlantic/South_Georgia, Brazil/DeNoronha, Etc/GMT+2, America/Scoresbysund, Atlantic/Azores, Atlantic/Cape_Verde, Etc/GMT+1, Africa/Abidjan, Africa/Accra, Africa/Bamako, Africa/Banjul, Africa/Bissau, Africa/Casablanca, Africa/Conakry, Africa/Dakar, Africa/El_Aaiun, Africa/Freetown, Africa/Lome, Africa/Monrovia, Africa/Nouakchott, Africa/Ouagadougou, Africa/Sao_Tome, Africa/Timbuktu, America/Danmarkshavn, Atlantic/Canary, Atlantic/Faeroe, Atlantic/Faroe, Atlantic/Madeira, Atlantic/Reykjavik, Atlantic/St_Helena, Eire, Etc/GMT, Etc/GMT+0, Etc/GMT-0, Etc/GMT0, Etc/Greenwich, Etc/UCT, Etc/UTC, Etc/Universal, Etc/Zulu, Europe/Belfast, Europe/Dublin, Europe/Guernsey, Europe/Isle_of_Man, Europe/Jersey, Europe/Lisbon, Europe/London, GB, GB-Eire, GMT, GMT0, Greenwich, Iceland, Portugal, UCT, UTC, Universal, WET, Zulu, Africa/Algiers, Africa/Bangui, Africa/Brazzaville, Africa/Ceuta, Africa/Douala, Africa/Kinshasa, Africa/Lagos, Africa/Libreville, Africa/Luanda, Africa/Malabo, Africa/Ndjamena, Africa/Niamey, Africa/Porto-Novo, Africa/Tunis, Africa/Windhoek, Arctic/Longyearbyen, Atlantic/Jan_Mayen, CET, ECT, Etc/GMT-1, Europe/Amsterdam, Europe/Andorra, Europe/Belgrade, Europe/Berlin, Europe/Bratislava, Europe/Brussels, Europe/Budapest, Europe/Copenhagen, Europe/Gibraltar, Europe/Ljubljana, Europe/Luxembourg, Europe/Madrid, Europe/Malta, Europe/Monaco, Europe/Oslo, Europe/Paris, Europe/Podgorica, Europe/Prague, Europe/Rome, Europe/San_Marino, Europe/Sarajevo, Europe/Skopje, Europe/Stockholm, Europe/Tirane, Europe/Vaduz, Europe/Vatican, Europe/Vienna, Europe/Warsaw, Europe/Zagreb, Europe/Zurich, MET, Poland, ART, Africa/Blantyre, Africa/Bujumbura, Africa/Cairo, Africa/Gaborone, Africa/Harare, Africa/Johannesburg, Africa/Kigali, Africa/Lubumbashi, Africa/Lusaka, Africa/Maputo, Africa/Maseru, Africa/Mbabane, Africa/Tripoli, Asia/Amman, Asia/Beirut, Asia/Damascus, Asia/Gaza, Asia/Istanbul, Asia/Jerusalem, Asia/Nicosia, Asia/Tel_Aviv, CAT, EET, Egypt, Etc/GMT-2, Europe/Athens, Europe/Bucharest, Europe/Chisinau, Europe/Helsinki, Europe/Istanbul, Europe/Kaliningrad, Europe/Kiev, Europe/Mariehamn, Europe/Minsk, Europe/Nicosia, Europe/Riga, Europe/Simferopol, Europe/Sofia, Europe/Tallinn, Europe/Tiraspol, Europe/Uzhgorod, Europe/Vilnius, Europe/Zaporozhye, Israel, Libya, Turkey, Africa/Addis_Ababa, Africa/Asmara, Africa/Asmera, Africa/Dar_es_Salaam, Africa/Djibouti, Africa/Kampala, Africa/Khartoum, Africa/Mogadishu, Africa/Nairobi, Antarctica/Syowa, Asia/Aden, Asia/Baghdad, Asia/Bahrain, Asia/Kuwait, Asia/Qatar, Asia/Riyadh, EAT, Etc/GMT-3, Europe/Moscow, Europe/Volgograd, Indian/Antananarivo, Indian/Comoro, Indian/Mayotte, W-SU, Asia/Riyadh87, Asia/Riyadh88, Asia/Riyadh89, Mideast/Riyadh87, Mideast/Riyadh88, Mideast/Riyadh89, Asia/Tehran, Iran, Asia/Baku, Asia/Dubai, Asia/Muscat, Asia/Tbilisi, Asia/Yerevan, Etc/GMT-4, Europe/Samara, Indian/Mahe, Indian/Mauritius, Indian/Reunion, NET, Asia/Kabul, Asia/Aqtau, Asia/Aqtobe, Asia/Ashgabat, Asia/Ashkhabad, Asia/Dushanbe, Asia/Karachi, Asia/Oral, Asia/Samarkand, Asia/Tashkent, Asia/Yekaterinburg, Etc/GMT-5, Indian/Kerguelen, Indian/Maldives, PLT, Asia/Calcutta, Asia/Colombo, Asia/Kolkata, IST, Asia/Kathmandu, Asia/Katmandu, Antarctica/Mawson, Antarctica/Vostok, Asia/Almaty, Asia/Bishkek, Asia/Dacca, Asia/Dhaka, Asia/Novosibirsk, Asia/Omsk, Asia/Qyzylorda, Asia/Thimbu, Asia/Thimphu, BST, Etc/GMT-6, Indian/Chagos, Asia/Rangoon, Indian/Cocos, Antarctica/Davis, Asia/Bangkok, Asia/Ho_Chi_Minh, Asia/Hovd, Asia/Jakarta, Asia/Krasnoyarsk, Asia/Phnom_Penh, Asia/Pontianak, Asia/Saigon, Asia/Vientiane, Etc/GMT-7, Indian/Christmas, VST, Antarctica/Casey, Asia/Brunei, Asia/Choibalsan, Asia/Chongqing, Asia/Chungking, Asia/Harbin, Asia/Hong_Kong, Asia/Irkutsk, Asia/Kashgar, Asia/Kuala_Lumpur, Asia/Kuching, Asia/Macao, Asia/Macau, Asia/Makassar, Asia/Manila, Asia/Shanghai, Asia/Singapore, Asia/Taipei, Asia/Ujung_Pandang, Asia/Ulaanbaatar, Asia/Ulan_Bator, Asia/Urumqi, Australia/Perth, Australia/West, CTT, Etc/GMT-8, Hongkong, PRC, Singapore, Australia/Eucla, Asia/Dili, Asia/Jayapura, Asia/Pyongyang, Asia/Seoul, Asia/Tokyo, Asia/Yakutsk, Etc/GMT-9, JST, Japan, Pacific/Palau, ROK, ACT, Australia/Adelaide, Australia/Broken_Hill, Australia/Darwin, Australia/North, Australia/South, Australia/Yancowinna, AET, Antarctica/DumontDUrville, Asia/Sakhalin, Asia/Vladivostok, Australia/ACT, Australia/Brisbane, Australia/Canberra, Australia/Currie, Australia/Hobart, Australia/Lindeman, Australia/Melbourne, Australia/NSW, Australia/Queensland, Australia/Sydney, Australia/Tasmania, Australia/Victoria, Etc/GMT-10, Pacific/Guam, Pacific/Port_Moresby, Pacific/Saipan, Pacific/Truk, Pacific/Yap, Australia/LHI, Australia/Lord_Howe, Asia/Magadan, Etc/GMT-11, Pacific/Efate, Pacific/Guadalcanal, Pacific/Kosrae, Pacific/Noumea, Pacific/Ponape, SST, Pacific/Norfolk, Antarctica/McMurdo, Antarctica/South_Pole, Asia/Anadyr, Asia/Kamchatka, Etc/GMT-12, Kwajalein, NST, NZ, Pacific/Auckland, Pacific/Fiji, Pacific/Funafuti, Pacific/Kwajalein, Pacific/Majuro, Pacific/Nauru, Pacific/Tarawa, Pacific/Wake, Pacific/Wallis, NZ-CHAT, Pacific/Chatham, Etc/GMT-13, Pacific/Enderbury, Pacific/Tongatapu, Etc/GMT-14, Pacific/Kiritimati]


The above Java code is relatively simple and it does the job of providing the valid TimeZone IDs available to me in a given environment, but Groovy can be even simpler to use in situations such as this one. The next code listing is a simple Groovy script for printing out these values.

displayTimeZoneIds1.groovy

println Arrays.toString(TimeZone.availableIDs)


Wow! It doesn't get much simpler than that. Groovy allowed me to drop the class declaration, the main method declaration, both java.util imports, and even simplified the "meaty" line from the Java code. It turns out that it actually does gets even easier, however, as shown in the next version of the Groovy-based script.

displayTimeZoneIds2.groovy

println TimeZone.availableIDs


The second version of the Groovy script dropped use of Arrays.toString(Object) because it is unnecessary when using Groovy arrays. Now that's super simple: println TimeZone.availableIDs

The static method Arrays.toString(Object[]) was necessary in the Java example to avoid seeing the default Object.toString() implementation for the array of Strings that would look something like this: [Ljava.lang.String;@3e25a5 where the [ indicates an array, the L indicates a reference type (not primitive), java.lang.String is obviously the class name of the elements of the array, and the @3e25a5 represents the "unsigned hexadecimal representation" of the object's hash code.

With things so simple, it seems like it's a good time to add some additional functionality. We'll do one more very simple thing that will dramatically improve the readability of our output. The third version of the Groovy script leverages Groovy's closure support to easy iterate over the String array and print each element of the array (each TimeZone ID) on its own line and without the array delimiting square braces ([]).

displayTimeZoneIds3.groovy

TimeZone.availableIDs.each {println it}


It may seem almost too good to be true, but here's the output:


Etc/GMT+12
Etc/GMT+11
MIT
Pacific/Apia
Pacific/Midway
Pacific/Niue
Pacific/Pago_Pago
Pacific/Samoa
US/Samoa
America/Adak
America/Atka
Etc/GMT+10
HST
Pacific/Fakaofo
Pacific/Honolulu
Pacific/Johnston
Pacific/Rarotonga
Pacific/Tahiti
SystemV/HST10
US/Aleutian
US/Hawaii
Pacific/Marquesas
AST
America/Anchorage
America/Juneau
America/Nome
America/Yakutat
Etc/GMT+9
Pacific/Gambier
SystemV/YST9
SystemV/YST9YDT
US/Alaska
America/Dawson
America/Ensenada
America/Los_Angeles
America/Tijuana
America/Vancouver
America/Whitehorse
Canada/Pacific
Canada/Yukon
Etc/GMT+8
Mexico/BajaNorte
PST
PST8PDT
Pacific/Pitcairn
SystemV/PST8
SystemV/PST8PDT
US/Pacific
US/Pacific-New
America/Boise
America/Cambridge_Bay
America/Chihuahua
America/Dawson_Creek
America/Denver
America/Edmonton
America/Hermosillo
America/Inuvik
America/Mazatlan
America/Phoenix
America/Shiprock
America/Yellowknife
Canada/Mountain
Etc/GMT+7
MST
MST7MDT
Mexico/BajaSur
Navajo
PNT
SystemV/MST7
SystemV/MST7MDT
US/Arizona
US/Mountain
America/Belize
America/Cancun
America/Chicago
America/Costa_Rica
America/El_Salvador
America/Guatemala
America/Indiana/Knox
America/Indiana/Tell_City
America/Knox_IN
America/Managua
America/Menominee
America/Merida
America/Mexico_City
America/Monterrey
America/North_Dakota/Center
America/North_Dakota/New_Salem
America/Rainy_River
America/Rankin_Inlet
America/Regina
America/Swift_Current
America/Tegucigalpa
America/Winnipeg
CST
CST6CDT
Canada/Central
Canada/East-Saskatchewan
Canada/Saskatchewan
Chile/EasterIsland
Etc/GMT+6
Mexico/General
Pacific/Easter
Pacific/Galapagos
SystemV/CST6
SystemV/CST6CDT
US/Central
US/Indiana-Starke
America/Atikokan
America/Bogota
America/Cayman
America/Coral_Harbour
America/Detroit
America/Fort_Wayne
America/Grand_Turk
America/Guayaquil
America/Havana
America/Indiana/Indianapolis
America/Indiana/Marengo
America/Indiana/Petersburg
America/Indiana/Vevay
America/Indiana/Vincennes
America/Indiana/Winamac
America/Indianapolis
America/Iqaluit
America/Jamaica
America/Kentucky/Louisville
America/Kentucky/Monticello
America/Lima
America/Louisville
America/Montreal
America/Nassau
America/New_York
America/Nipigon
America/Panama
America/Pangnirtung
America/Port-au-Prince
America/Resolute
America/Thunder_Bay
America/Toronto
Canada/Eastern
Cuba
EST
EST5EDT
Etc/GMT+5
IET
Jamaica
SystemV/EST5
SystemV/EST5EDT
US/East-Indiana
US/Eastern
US/Michigan
America/Caracas
America/Anguilla
America/Antigua
America/Argentina/San_Luis
America/Aruba
America/Asuncion
America/Barbados
America/Blanc-Sablon
America/Boa_Vista
America/Campo_Grande
America/Cuiaba
America/Curacao
America/Dominica
America/Eirunepe
America/Glace_Bay
America/Goose_Bay
America/Grenada
America/Guadeloupe
America/Guyana
America/Halifax
America/La_Paz
America/Manaus
America/Marigot
America/Martinique
America/Moncton
America/Montserrat
America/Port_of_Spain
America/Porto_Acre
America/Porto_Velho
America/Puerto_Rico
America/Rio_Branco
America/Santiago
America/Santo_Domingo
America/St_Barthelemy
America/St_Kitts
America/St_Lucia
America/St_Thomas
America/St_Vincent
America/Thule
America/Tortola
America/Virgin
Antarctica/Palmer
Atlantic/Bermuda
Atlantic/Stanley
Brazil/Acre
Brazil/West
Canada/Atlantic
Chile/Continental
Etc/GMT+4
PRT
SystemV/AST4
SystemV/AST4ADT
America/St_Johns
CNT
Canada/Newfoundland
AGT
America/Araguaina
America/Argentina/Buenos_Aires
America/Argentina/Catamarca
America/Argentina/ComodRivadavia
America/Argentina/Cordoba
America/Argentina/Jujuy
America/Argentina/La_Rioja
America/Argentina/Mendoza
America/Argentina/Rio_Gallegos
America/Argentina/Salta
America/Argentina/San_Juan
America/Argentina/Tucuman
America/Argentina/Ushuaia
America/Bahia
America/Belem
America/Buenos_Aires
America/Catamarca
America/Cayenne
America/Cordoba
America/Fortaleza
America/Godthab
America/Jujuy
America/Maceio
America/Mendoza
America/Miquelon
America/Montevideo
America/Paramaribo
America/Recife
America/Rosario
America/Santarem
America/Sao_Paulo
Antarctica/Rothera
BET
Brazil/East
Etc/GMT+3
America/Noronha
Atlantic/South_Georgia
Brazil/DeNoronha
Etc/GMT+2
America/Scoresbysund
Atlantic/Azores
Atlantic/Cape_Verde
Etc/GMT+1
Africa/Abidjan
Africa/Accra
Africa/Bamako
Africa/Banjul
Africa/Bissau
Africa/Casablanca
Africa/Conakry
Africa/Dakar
Africa/El_Aaiun
Africa/Freetown
Africa/Lome
Africa/Monrovia
Africa/Nouakchott
Africa/Ouagadougou
Africa/Sao_Tome
Africa/Timbuktu
America/Danmarkshavn
Atlantic/Canary
Atlantic/Faeroe
Atlantic/Faroe
Atlantic/Madeira
Atlantic/Reykjavik
Atlantic/St_Helena
Eire
Etc/GMT
Etc/GMT+0
Etc/GMT-0
Etc/GMT0
Etc/Greenwich
Etc/UCT
Etc/UTC
Etc/Universal
Etc/Zulu
Europe/Belfast
Europe/Dublin
Europe/Guernsey
Europe/Isle_of_Man
Europe/Jersey
Europe/Lisbon
Europe/London
GB
GB-Eire
GMT
GMT0
Greenwich
Iceland
Portugal
UCT
UTC
Universal
WET
Zulu
Africa/Algiers
Africa/Bangui
Africa/Brazzaville
Africa/Ceuta
Africa/Douala
Africa/Kinshasa
Africa/Lagos
Africa/Libreville
Africa/Luanda
Africa/Malabo
Africa/Ndjamena
Africa/Niamey
Africa/Porto-Novo
Africa/Tunis
Africa/Windhoek
Arctic/Longyearbyen
Atlantic/Jan_Mayen
CET
ECT
Etc/GMT-1
Europe/Amsterdam
Europe/Andorra
Europe/Belgrade
Europe/Berlin
Europe/Bratislava
Europe/Brussels
Europe/Budapest
Europe/Copenhagen
Europe/Gibraltar
Europe/Ljubljana
Europe/Luxembourg
Europe/Madrid
Europe/Malta
Europe/Monaco
Europe/Oslo
Europe/Paris
Europe/Podgorica
Europe/Prague
Europe/Rome
Europe/San_Marino
Europe/Sarajevo
Europe/Skopje
Europe/Stockholm
Europe/Tirane
Europe/Vaduz
Europe/Vatican
Europe/Vienna
Europe/Warsaw
Europe/Zagreb
Europe/Zurich
MET
Poland
ART
Africa/Blantyre
Africa/Bujumbura
Africa/Cairo
Africa/Gaborone
Africa/Harare
Africa/Johannesburg
Africa/Kigali
Africa/Lubumbashi
Africa/Lusaka
Africa/Maputo
Africa/Maseru
Africa/Mbabane
Africa/Tripoli
Asia/Amman
Asia/Beirut
Asia/Damascus
Asia/Gaza
Asia/Istanbul
Asia/Jerusalem
Asia/Nicosia
Asia/Tel_Aviv
CAT
EET
Egypt
Etc/GMT-2
Europe/Athens
Europe/Bucharest
Europe/Chisinau
Europe/Helsinki
Europe/Istanbul
Europe/Kaliningrad
Europe/Kiev
Europe/Mariehamn
Europe/Minsk
Europe/Nicosia
Europe/Riga
Europe/Simferopol
Europe/Sofia
Europe/Tallinn
Europe/Tiraspol
Europe/Uzhgorod
Europe/Vilnius
Europe/Zaporozhye
Israel
Libya
Turkey
Africa/Addis_Ababa
Africa/Asmara
Africa/Asmera
Africa/Dar_es_Salaam
Africa/Djibouti
Africa/Kampala
Africa/Khartoum
Africa/Mogadishu
Africa/Nairobi
Antarctica/Syowa
Asia/Aden
Asia/Baghdad
Asia/Bahrain
Asia/Kuwait
Asia/Qatar
Asia/Riyadh
EAT
Etc/GMT-3
Europe/Moscow
Europe/Volgograd
Indian/Antananarivo
Indian/Comoro
Indian/Mayotte
W-SU
Asia/Riyadh87
Asia/Riyadh88
Asia/Riyadh89
Mideast/Riyadh87
Mideast/Riyadh88
Mideast/Riyadh89
Asia/Tehran
Iran
Asia/Baku
Asia/Dubai
Asia/Muscat
Asia/Tbilisi
Asia/Yerevan
Etc/GMT-4
Europe/Samara
Indian/Mahe
Indian/Mauritius
Indian/Reunion
NET
Asia/Kabul
Asia/Aqtau
Asia/Aqtobe
Asia/Ashgabat
Asia/Ashkhabad
Asia/Dushanbe
Asia/Karachi
Asia/Oral
Asia/Samarkand
Asia/Tashkent
Asia/Yekaterinburg
Etc/GMT-5
Indian/Kerguelen
Indian/Maldives
PLT
Asia/Calcutta
Asia/Colombo
Asia/Kolkata
IST
Asia/Kathmandu
Asia/Katmandu
Antarctica/Mawson
Antarctica/Vostok
Asia/Almaty
Asia/Bishkek
Asia/Dacca
Asia/Dhaka
Asia/Novosibirsk
Asia/Omsk
Asia/Qyzylorda
Asia/Thimbu
Asia/Thimphu
BST
Etc/GMT-6
Indian/Chagos
Asia/Rangoon
Indian/Cocos
Antarctica/Davis
Asia/Bangkok
Asia/Ho_Chi_Minh
Asia/Hovd
Asia/Jakarta
Asia/Krasnoyarsk
Asia/Phnom_Penh
Asia/Pontianak
Asia/Saigon
Asia/Vientiane
Etc/GMT-7
Indian/Christmas
VST
Antarctica/Casey
Asia/Brunei
Asia/Choibalsan
Asia/Chongqing
Asia/Chungking
Asia/Harbin
Asia/Hong_Kong
Asia/Irkutsk
Asia/Kashgar
Asia/Kuala_Lumpur
Asia/Kuching
Asia/Macao
Asia/Macau
Asia/Makassar
Asia/Manila
Asia/Shanghai
Asia/Singapore
Asia/Taipei
Asia/Ujung_Pandang
Asia/Ulaanbaatar
Asia/Ulan_Bator
Asia/Urumqi
Australia/Perth
Australia/West
CTT
Etc/GMT-8
Hongkong
PRC
Singapore
Australia/Eucla
Asia/Dili
Asia/Jayapura
Asia/Pyongyang
Asia/Seoul
Asia/Tokyo
Asia/Yakutsk
Etc/GMT-9
JST
Japan
Pacific/Palau
ROK
ACT
Australia/Adelaide
Australia/Broken_Hill
Australia/Darwin
Australia/North
Australia/South
Australia/Yancowinna
AET
Antarctica/DumontDUrville
Asia/Sakhalin
Asia/Vladivostok
Australia/ACT
Australia/Brisbane
Australia/Canberra
Australia/Currie
Australia/Hobart
Australia/Lindeman
Australia/Melbourne
Australia/NSW
Australia/Queensland
Australia/Sydney
Australia/Tasmania
Australia/Victoria
Etc/GMT-10
Pacific/Guam
Pacific/Port_Moresby
Pacific/Saipan
Pacific/Truk
Pacific/Yap
Australia/LHI
Australia/Lord_Howe
Asia/Magadan
Etc/GMT-11
Pacific/Efate
Pacific/Guadalcanal
Pacific/Kosrae
Pacific/Noumea
Pacific/Ponape
SST
Pacific/Norfolk
Antarctica/McMurdo
Antarctica/South_Pole
Asia/Anadyr
Asia/Kamchatka
Etc/GMT-12
Kwajalein
NST
NZ
Pacific/Auckland
Pacific/Fiji
Pacific/Funafuti
Pacific/Kwajalein
Pacific/Majuro
Pacific/Nauru
Pacific/Tarawa
Pacific/Wake
Pacific/Wallis
NZ-CHAT
Pacific/Chatham
Etc/GMT-13
Pacific/Enderbury
Pacific/Tongatapu
Etc/GMT-14
Pacific/Kiritimati


Before finishing this example, I would like to do one more thing to make the returned set of TimeZone IDs easier to manage. It can be difficult to find what I'm looking for in this list and I'd rather see it in sorted form.

displayTimeZoneIds4.groovy

TimeZone.availableIDs.sort().each {println it}


That's all there is to it -- there's nothing sordid about that. I don't show it here, but running that single line script will return the available TimeZone IDs without the array syntax, with each ID on its own line, in an alphabetically sorted order.


Conclusion

This blog post had two primary purposes. One purpose was to demonstrate another practical example of where Groovy can greatly simplify use of basic Java APIs, especially when simply trying to learn about something new or get a listing of options available in a given environment. A second purpose was to demonstrate how to find out which TimeZone IDs are available, though this was really more of an excuse to demonstrate the sleekness of Groovy.

Thursday, February 18, 2010

RMOUG Training Days 2010: It's A Wrap!

Several blog posts related to Rocky Mountain Oracle Users Group (RMOUG) Training Days 2010 have started appearing and some of the presenters have made their slides available for download. In this blog post, I collect links to some of those blog posts and presentation slides after briefly summarizing my own experience.

I enjoyed presenting "RESTful Java" and "Applied Groovy: Scripting for Java Development" at this year's (2010) edition of RMOUG Training Days. The audiences for these presentations were small (approximately 10 in the REST presentation and 20 to 30 in the Groovy presentation), but the people in attendance were attentive and asked great questions. I have always believed, and have heard many other speakers also say, that the types of questions from the audience is a good type of feedback on how well the presentation went. Some of the questions were so insightful and useful that I plan to blog on the answers to those questions in future blog posts.

Riyaj Shamsudeen was not able to make the flight to Denver due to illness, but he has made his presentations ("Why Does Optimizer Hate My SQL?" and "Advanced RAC Troubleshooting") available online.

Mike Ault, another speaker, posted Day 1 of RMOUG and Ready for Day 2 at RMOUG. Kellyn Pedersen has blogged about her experience speaking at RMOUG Training Days for the first time in the post RMOUG 2010. Brent Lowe's recap is contained in his blog post RMOUG 2010 Recap.

Mark Rittman summarizes his experiences at his first RMOUG Training Days conference in the blog post OWB, RMOUG and ODTUG in Denver. He mentions that the materials associated with the presentation he and Stewart Bryson gave on "OWB11gR2 New Features for DBAs and Developers" are available on the Rittman Mead Consulting articles page.

It is likely that there will be additional posts on RMOUG Training Days 2010 in the next several days. I'm already looking forward to RMOUG Training Days 2011.

Wednesday, February 17, 2010

More Groovy-based Simple HTTP Clients

In my previous blog post, I briefly wrote about the use of Groovy, Groovy's GDK String, and the Java-provided URL class to write simple HTTP/REST clients. In this post, I look at how use of the URLConnection returned from a call to URL.openConnection() can be used to gain even more details about the HTTP connection.

The URLConnection class and its child HttpURLConnection class provide many useful methods for accessing details regarding a HTTP connection. These are easily obtained from a call to URL.openConnection(). This is demonstrated in the next Groovy code listing.


#!/usr/bin/env groovy
def NEW_LINE = System.getProperty("line.separator")
if (args.length < 1)
{
println "Enter a URL as an argument."
System.exit(-1)
}
def address = args[0]
def urlInfo = address.toURL()
println "URL: ${address}${NEW_LINE}"
println "Host/Port: ${urlInfo.host}/${urlInfo.port}${NEW_LINE}"
println "Protocol: ${urlInfo.protocol}${NEW_LINE}"

def connection = urlInfo.openConnection()
println "Connection Type: ${connection.class}"
println "Content Type: ${connection.contentType}"
println "Response Code/Message: ${connection.responseCode} / ${connection.responseMessage}"
println "Request Method: ${connection.requestMethod}"
println "Date: ${connection.date}"
println "Last-Modified: ${connection.lastModified}"
println "Content Length: ${connection.contentLength}"


The above code leads to output like that shown in the following screen snapshot where I run the script against three URLs I often hit from my web browser: http://www.javaworld.com/, http://java.net/, and http://www.oracle.com/technology/index.html.



The response code and message are included in the output shown in the above screen snapshot. In all three cases, the response code and message are 200 and OK respectively. The content length of -1 is the value provided by the getContentLength method when (according to the Javadoc) "content length is not known."

In the example above, I used the convenience methods provided for "certain header fields [that] are accessed frequently" (see URLConnection Javadoc description). Because many more header fields are included in responses, it can be useful to find out what these are and see their values. This is demonstrated in the next code sample.


#!/usr/bin/env groovy
def NEW_LINE = System.getProperty("line.separator")
if (args.length < 1)
{
println "Enter a URL as an argument."
System.exit(-1)
}
def address = args[0]
def urlInfo = address.toURL()
println "===================================================================="
println "====== HEADER FIELDS FOR URL ${address}"
println "===================================================================="
def connection = urlInfo.openConnection()
headerFields = connection.getHeaderFields()
headerFields.each {println it}


The simple Groovy script above uses the GDK Map.each method that acts on closures and prints out the field values for each header field. The output from running this script against the same three URLs as before is shown next.



This output shows that there are some differences in the fields returned by each of the three URLs in response to the implicit GET request.

The combination of Groovy, Groovy GDK, URL, and HttpURLConnection make it easy to generate a quick and dirty HTTP client. This can be useful in testing HTTP-exposed services including HTTP-based REST services.

Tuesday, February 16, 2010

Minimalistic HTTP Clients with Groovy

The Rocky Mountain Oracle Users Group (RMOUG) Training Days 2010 main technical sessions are going to be held tomorrow and Thursday of this week and I will be presenting two presentations on Groovy and JAX-RS/REST at this conference. It seems like a good time to use Groovy and JAX-RS/REST in the same blog post.

In my previous blog posts on JAX-RS and REST, I have often used RESTClient as the client for testing my deployed services. RESTClient provides a nice graphical user interface and is easy to use. However, if I only need command line test, Groovy is one easy option.

With Groovy, it is easy to take advantage of Java's URI and URL classes. Groovy makes it even easier to use these than in traditional Java because the Groovy GDK-provided String class provides the very convenient toURI() and toURL methods. This is such an easy process that it's actually easier to demonstrate it than to attempt to describe it. So, here is an example in two lines of Groovy code (really one line if you don't count the shebang line):


#!/usr/bin/env groovy
println "http://localhost:8080/rest/resources/movies/2/2".toURL().text


The URL incorporated in the String in the above code points to a JAX-RS-powered REST-based web service I discussed in a previous blog post. The simple service behind that URL returns a "movie of the day" for the month and date given by the two integers at the end of the URL. When the above Groovy code is executed, the output appears as shown in the next screen snapshot.



That simple Groovy code of one real executable line leveraged the power of the Groovy GDK's String and a standard Java class to easily provide the text provided by the resource at the other end of the URL. It doesn't get any easier than that.

The next example is slightly more interesting, but still only requires a small number of lines of Groovy code:


#!/usr/bin/env groovy
address = "http://localhost:8080/rest/resources/movies/2/2"
println "URL: ${address.toURL().text}"
println ""
println "URI: ${address.toURI().toURL().text}"


The output from the above script is shown next.



Because Java's URL class provides significantly more details than just the text available from accessing that URL, this information is readily accessed in a Groovy script. An example of some of this additional information being retrieved is shown in the next code listing and associated output screen snapshot.


#!/usr/bin/env groovy
def NEW_LINE = System.getProperty("line.separator")
def address = "http://localhost:8080/rest/resources/movies/2/2"
def urlInfo = address.toURL()
println "URL: ${address}${NEW_LINE}"
println "URL Text: ${urlInfo.text}${NEW_LINE}"
println "Host/Port: ${urlInfo.host}/${urlInfo.port}${NEW_LINE}"
println "Protocol: ${urlInfo.protocol}${NEW_LINE}"
println "User Info: ${urlInfo.userInfo}${NEW_LINE}"
println "File: ${urlInfo.file}${NEW_LINE}"




For the final simple example in this post, I make the simple script above a little more dynamic by using the first argument to the script as the URL from which to access details.


#!/usr/bin/env groovy
def NEW_LINE = System.getProperty("line.separator")
if (args.length < 1)
{
println "Enter a URL as an argument."
System.exit(-1)
}
def address = args[0]
def urlInfo = address.toURL()
println "URL: ${address}${NEW_LINE}"
println "URL Text: ${urlInfo.text}${NEW_LINE}"
println "Host/Port: ${urlInfo.host}/${urlInfo.port}${NEW_LINE}"
println "Protocol: ${urlInfo.protocol}${NEW_LINE}"
println "User Info: ${urlInfo.userInfo}${NEW_LINE}"
println "File: ${urlInfo.file}${NEW_LINE}"


The corresponding output is shown in the next screen snapshot.



In this post, I have demonstrated how easy it is to mix the best of Groovy with the best of Java to put together a very simple client for accessing HTTP-exposed services such as one finds in many REST applications. I didn't even get to the URL.openConnection() method here, but this method opens up a great deal more information that might be useful for simple testing tools when using HTTP-exposed services.

Monday, February 15, 2010

JAX-RS and HTTP Responses

HTTP Status Codes are a big part of the HTTP protocol that most of us have seen countless times in our web browsing and development. We are used to seeing 404 (Not Found), 200 (OK), and so forth. Because HTTP is often closely tied with REST style applications, it is not surprising that JAX-RS provides tremendous support for returning appropriate HTTP responses from REST-based web services. In this blog post, I look at the tip of the iceberg in terms of JAX-RS support for HTTP-based service responses.

The code listing that follows would not generally be a useful piece of functionality, but it is used here to demonstrate how easy it is with JAX-RS to generate HTTP responses with specific HTTP status codes.


@Path("/responses")
public class ResponseMaker
{
/**
* Generates an HTTP response based on the String provided as part of this
* resource's URI.
*
* @param responseString Portion of this resource's URI from which the
* particular response will be generated.
* @return Response based on provided URI portion.
*/
@GET
@Path("/{responseString}")
@Consumes("text/plain")
@Produces("text/html")
public Response getResponse(
@PathParam("responseString") final String responseString)
{
final String desiredResponse = responseString != null
? responseString.trim().toUpperCase()
: Status.BAD_REQUEST.name();
Status status = null;
try
{
status = Status.valueOf(desiredResponse);
}
catch (IllegalArgumentException illegalArgEx)
{
status = Status.BAD_REQUEST;
}
return Response.status(status).type(MediaType.TEXT_HTML_TYPE).build();
}
}


The above code uses JAX-RS annotations to indicate which HTTP method corresponds to the Java method in the code listing (@GET indicates HTTP GET method). This simple method accepts a String and, if the String matches one of the Response.Status enum value's string representations, that Status is set. Otherwise, a default of BAD_REQUEST is selected. The Response is built with that status and with a MediaType of TEXT_HTML_TYPE.

I use RESTClient as an easy client tool to demonstrate this simple JAX-RS web service in action. The next two screen snapshots show what RESTClient sees (and what any other client would see) in terms of responses to certain provided URIs (shown at top of GUI).






JAX-RS also makes it easy to turn encountered exceptions into HTTP response codes indicating the error. The next piece of code demonstrates this.


/**
* Return an exception-based HTTP response based on the provided number
* indicating a particular exception to be used. The HTTP method PUT would
* normally likely not be the method used for this type of operation, but
* it makes it easy to differentiate from the other method in this class
* already tied to @GET and also accepting a single String.
*
* There are four cases in which a particular exception is used to build the
* response and that exception and a particular Response.Status are provided,
* telling the JAX-RS provider which HTTP status to tie to that particular
* thrown exception. In the default/general case when one of the first four
* are not used, no specific Response.Status is used, so the general 500
* Internal Server Error will be returned to the client along with the'
* exception's stack trace as the body.
*
* @param exceptionNumberType A number used to determine which type of
* exception is used for the basis of the response.
* @return Response based on the described exception type.
*/
@PUT
@Path("/{exceptionNumberType}")
@Consumes("text/plain")
public Response causeException(
@PathParam("exceptionNumberType") final int exceptionNumberType)
{
Exception exception;
Status status = null;
switch (exceptionNumberType)
{
case 1 : exception = new NullPointerException("1. Null Encountered.");
status = Status.NOT_FOUND;
break;
case 2 : exception = new IllegalArgumentException("2. Bad argument");
status = Status.PRECONDITION_FAILED;
break;
case 3 : exception = new RuntimeException("3. Runtime Exception");
status = Status.BAD_REQUEST;
break;
case 4 : exception = new NumberFormatException("4. Bad Numeric Format");
status = Status.NOT_ACCEPTABLE;
break;
default : exception = new Exception("General Exception");
}
throw status != null
? new WebApplicationException(exception, status)
: new WebApplicationException(exception);
}


I included some verbose comments on this method to describe how it behaves in greater detail. This method primarily demonstrates how the WebApplicationException is useful in turning Java exceptions into HTTP responses. I only use two of this exception's eight constructors in this example, but they demonstrate the difference between providing a particular HTTP Response.Status with the exception (and counting on the JAX-RS provider to place the results of Response.Status.getReasonPhrase() in the response body) or allowing the JAX-RS provider to associated a Response.Status with the exception (500) and showing the entire stack trace of the exception.

This difference is demonstrated in the next three screen snapshots. Two are of exceptions for which a particular (largely nonsensical here and only meant to illustrate how to do this) HTTP status code is associated with the exception and one is for the case where the JAX-RS provider assigns 500 implicitly and puts the exception's stack trace in the response body rather than the Response.Status.getReasonPhrase().







In this post, I have attempted to demonstrate how easy JAX-RS makes it to specify HTTP responses based on regular and exceptional conditions. I have only touched on a small part of the extensive JAX-RS support for building appropriate responses.

Playing with Jersey/JAX-RS Method Designators

There is a part of me that likes to try things just to see what happens. If I read something that says, "Never do this," I have a hard time not doing just that thing if I believe doing so comes at no significant cost. There can be value to this approach when I learn about the results associated with an improper action and can then more readily recognize the underlying cause when I inadvertently cause the same problem myself or see a colleague dealing with the same effects. In this blog post, I look at how JAX-RS 1.1 (specifically the Jersey implementation) handles some of the "don't do that" rules related to resource methods.

One JAX-RS Resource Method Designation Per Java Method

JAX-RS literature warns that only one JAX-RS method designation annotation is allowed per method in a Java class resource. In other words, I should only be able to apply @GET or @POST or @PUT or @DELETE to a method, but never apply more than one of them to the same method. This actually seems sensible, but it prevents some of the "trickery" one commonly saw in servlet applications where the doPost and doGet methods were set up so that one called the other or they both called the same piece of code.

To find out what the negative consequences are of specifying multiple JAX-RS method designators on the same method, I took the method addMovieOfTheDay from my previous blog post (JAX-RS and the REST Uniform Interface) and added the @POST annotation on top of its existing @PUT annotation. The altered code is shown next:

Two JAX-RS Method Designation Annotations for a Single Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}


The above code compiles without incident. This is not surprising because, as the JAX-RS 1.1 specification states (Section 3.3 Resource Models, emphasis added), "A request method designator is a runtime annotation that is annotated with the @HttpMethod annotation." However, when I try to deploy it following the steps outlined in my blog post JAX-RS with Jersey: An Introduction, I see an error in the GlassFish v3 web-based Administrative Console. A screen snapshot of that is shown next.



The output states that an IllegalStateException has been thrown and suggests seeing the logs for additional details. There is a very lengthy stack trace in the logs, but the most significant piece of it is this:


[#|2010-02-14T19:43:38.495-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A (sub-)resource method, public java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDay(java.lang.Integer,java.lang.Integer,java.lang.String), should have only one HTTP method designator. It currently has the following designators defined: [@javax.ws.rs.PUT(), @javax.ws.rs.POST()]|#]


With the context of the situation in mind, this is pretty straightforward: "only one HTTP method designation" is allowed for a resource method and the method "[addMovieOfTheDay(Integer,String)] currently has the designators ... @PUT ... @POST [defined]." Okay, so that is bad. Note to self: The advice to only use one method designator per class resource method should be heeded.

As I wrote this post, I verified that NetBeans 6.8 continues the tradition of creating new Java servlets with the automatically generated doPost and doGet methods calling the same protected method (processRequest in NetBeans 6.8, though I seem to recall this method had a different name previously, such as doProcess()). I decided the next thing to try was annotating two separate methods with respective annotations, but have them call the same underlying method. My assumption was that this would work fine. The code for this is shown in the next listing.

Two Designators/Two Methods But with Single Common Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}

/**
* "Common" method for adding/updating a movie of the day that is intended
* to support PUT and POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
private String addMovie(final Integer month, final Integer date, final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}


This code compiles without incident because I appended "Post" to the method name of the added method. However, I "forgot" to change the annotation to @Post for the new method and this became obvious when I tried to deploy to GlassFish. I saw the same general error as before in the console, but the logs had details on the new problem:


[#|2010-02-14T20:43:06.557-0700|SEVERE|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=25;_ThreadName=Thread-1;|A resource, class rmoug.td2010.rest.MovieOfTheDay, has ambiguous sub-resource method for HTTP method PUT, URI path template /{month}/{date}/{title}, and output mime-type: text/html. The problematic mime-type sets (as defined by @Produces annotation at Java methods addMovieOfTheDay and addMovieOfTheDayPost) are [text/html] and [text/html]|#]


This SEVERE-level log message is telling us that the two methods annotated with @PUT are ambiguous because they have the same URI path and same MIME type. One of these needs to be different to allow the JAX-RS provider to differentiate them. For our purposes, I simply change the new method addMovieOfTheDayPost to have the @POST annotation rather than the @PUT annotation.

Method Intended for POST Annotated with @POST

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovie(month, date, title);
}


The code again compiles and even deploys this time. I don't show it here, but using RESTClient to access the resource via PUT and via POST works for both HTTP methods. In short, it does appear that one can delegate functionality from two different Java methods associated with different HTTP methods to the same common method.

The last thing I wanted to try in this area was having one method call the other rather than two methods calling a third, common method. To try this, I changed the method added for POST support to simply call the method for PUT:

POST-Supporting Method Calling PUT-Supporting Method

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@PUT
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
if (moviesOfTheMonth != null)
{
moviesOfTheMonth.put(date, title);
}
return generateAddedMovieHtml(title);
}

/**
* Add a new entry or update an existing entry representing a movie of the day.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
public String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}


This approach compiled successfully, deployed successfully, and I was able to invoke both the PUT-supporting and POST-supporting Java methods successfully. So, the approach of having one method directly call the other seems to work as well if a developer wants to get around the limitation of not having multiple HTTP method designations on the same Java method.


Only Public Methods Allowed as Resource Methods

Section 3.3.1 ("Visibility") of the JAX-RS 1.1 specification states: "Only public methods may be exposed as resource methods. An implementation SHOULD warn users if a
non-public method carries a method designator or @Path annotation."

Let's see what happens with Jersey in this situation.

To test this one out, I change the just-added method supporting POST to be package scope by removing its public modifier.

Package-level @POST-annotated Method

/**
* Add a new entry or update an existing entry representing a movie of the day
* via HTTP POST.
*
* @param month Month for the movie of the day.
* @param date Day of the month for the movie of the day.
* @param title Title of the movie of the day.
* @return HTML excerpt for movie of the day entry status.
*/
@POST
@Path("/{month}/{date}/{title}")
@Consumes("text/plain")
@Produces("text/html")
/*public*/ String addMovieOfTheDayPost(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date,
@PathParam("title") final String title)
{
return addMovieOfTheDay(month, date, title);
}


In this case, the code compiles and the WAR deploys without apparent incident, but things go less smoothly when a client attempts to use POST to add/change a movie. The package-level method is not available and this is made evident in the RESTClient GUI as shown in the next screen snapshot.



As the above image demonstrates, an HTTP response code of 405 ("Method Not Allowed") is returned when there is no public method associated with the specified HTTP method. As the specification suggests, Jersey does warn of this condition with the following WARNING-level logged statement:


[#|2010-02-14T21:13:31.226-0700|WARNING|glassfishv3.0|com.sun.jersey.server.impl.application.WebApplicationImpl|_ThreadID=28;_ThreadName=Thread-1;|A sub-resource method, java.lang.String rmoug.td2010.rest.MovieOfTheDay.addMovieOfTheDayPost(java.lang.Integer,java.lang.Integer,java.lang.String), MUST be public scoped otherwise the method is ignored|#]



Conclusion

The messages Jersey logs related to problems associated with resource method designations are relatively clear and straightforward. That is good news because it means that when these errors are unintentionally caused, they are more likely to be readily identified and addressed.