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.

Saturday, February 13, 2010

JAX-RS and the REST Uniform Interface

In Roy Fielding's dissertation that started it all, he stated:

"The central feature that distinguishes the REST architectural style from other network-based styles is its emphasis on a uniform interface between components."


The above quotation can be found in Section 5.1.5 ("Uniform Interface") of Chapter 5 ("Representational State Transfer (REST)") of Fielding's now-famous PhD dissertation "Architectural Styles and the Design of Network-based Software Architectures."

The "uniform interface" concept is obviously important to REST and is a driving force behind the design of HTTP methods. JAX-RS provides sophisticated annotations-based support for providing uniform interfaces to clients from regular Java classes. The main intent of this post is to briefly cover how JAX-RS makes it easy to write Java classes that provide this uniform interface.

Nearly all REST-based applications "happen" to be HTTP based as well, but it is often emphasized that HTTP is not absolutely required to implement a REST-based application. Still, HTTP and REST often do go together, so it is not surprising that the JAX-RS-provided annotations @GET, @DELETE, @POST, and @PUT closely mirror in name the respective HTTP methods GET, DELETE, POST, and PUT. Note that JAX-RS also provides @OPTIONS and @HEAD annotations.

In my blog post JAX-RS with Jersey: An Introduction, I demonstrated use of the @GET annotation in conjunction with fellow JAX-RS annotations @Path and @PathParam. Only data retrieval (hence the "GET") was shown in that post. In this post, I expand upon (and change) the MovieOfTheDay class I used in that post to support creation/insertion of new data, updating of existing data, and deletion/removal of data.

As I stated in the previous post, I use an (slightly modified for this post) internal static map to emulate what would normally be a datastore such as a database. This piece of code is shown next.

MovieOfTheDay.java Fragment: Emulated Data Storage Portion

/** Overall simulated database of movies of the day. */
private static final Map<Integer, Map<Integer, String>> MOVIE_OF_THE_DAY;

static
{
MOVIE_OF_THE_DAY = new ConcurrentHashMap<Integer, Map<Integer, String>>();

final Map<Integer, String> janMovies = new ConcurrentHashMap<Integer, String>();
janMovies.put(10, "Trading Places");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JANUARY), janMovies);

final Map<Integer, String> febMovies = new ConcurrentHashMap<Integer, String>();
febMovies.put(2, "Groundhog Day");
febMovies.put(13, "Casablanca");
febMovies.put(14, "Sleepless in Seattle");
febMovies.put(15, "How to Lose a Guy in 10 Days");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.FEBRUARY), febMovies);

final Map<Integer, String> marMovies = new ConcurrentHashMap<Integer, String>();
marMovies.put(16, "The Fugitive");
marMovies.put(17, "Darby O'Gill and the Little People");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.MARCH), marMovies);

final Map<Integer, String> aprMovies = new ConcurrentHashMap<Integer, String>();
aprMovies.put(25, "Raiders of the Lost Ark");
aprMovies.put(26, "Indiana Jones and the Temple of Doom");
aprMovies.put(27, "Indiana Jones and the Last Crusade");
aprMovies.put(28, "Indiana Jones and the Kingdom of the Crystal Skull");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.APRIL), aprMovies);

final Map<Integer, String> mayMovies = new ConcurrentHashMap<Integer, String>();
mayMovies.put(26, "Star Wars: Episode 1 - The Phantom Menace");
mayMovies.put(27, "Star Wars: Episode 2 - Attack of the Clones");
mayMovies.put(28, "Star Wars: Episode 3 - Revenge of the Sith");
mayMovies.put(29, "Star Wars: Episode 4 - A New Hope");
mayMovies.put(30, "Star Wars: Episode 5 - The Empire Strikes Back");
mayMovies.put(31, "Star Wars: Episode 6 - Return of the Jedi");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.MAY), mayMovies);

final Map<Integer, String> junMovies = new ConcurrentHashMap<Integer, String>();
junMovies.put(15, "The Great Outdoors");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JUNE), junMovies);

final Map<Integer, String> julMovies = new ConcurrentHashMap<Integer, String>();
julMovies.put(4, "Independence Day");
julMovies.put(21, "Men in Black");
julMovies.put(22, "Men in Black 2");
julMovies.put(23, "I, Robot");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JULY), julMovies);

final Map<Integer, String> augMovies = new ConcurrentHashMap<Integer, String>();
augMovies.put(10, "North by Northwest");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.AUGUST), augMovies);

final Map<Integer, String> sepMovies = new ConcurrentHashMap<Integer, String>();
sepMovies.put(1, "Dead Poets Society");
sepMovies.put(20, "Uncle Buck");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.SEPTEMBER), sepMovies);

final Map<Integer, String> octMovies = new ConcurrentHashMap<Integer, String>();
octMovies.put(28, "Psycho");
octMovies.put(29, "Sixth Sense");
octMovies.put(30, "Ghostbusters");
octMovies.put(31, "Young Frankenstein");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.OCTOBER), octMovies);

final Map<Integer, String> novMovies = new ConcurrentHashMap<Integer, String>();
novMovies.put(10, "The Italian Job");
novMovies.put(11, "Ocean's Eleven");
novMovies.put(12, "Ocean's Twelve");
novMovies.put(13, "Ocean's Thirteen");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.NOVEMBER), novMovies);

final Map<Integer, String> decMovies = new ConcurrentHashMap<Integer, String>();
decMovies.put(1, "Holiday Inn");
decMovies.put(24, "It's A Wonderful Life");
decMovies.put(25, "A Christmas Carol");
decMovies.put(26, "A Christmas Story");
decMovies.put(27, "Home Alone");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.DECEMBER), decMovies);
}


The above code snippet sets up an underlying data store with the movies of the day. The MovieOfTheDay class represents a resource and its class declaration is shown in the next code listing. The @Path annotation is significant because it tells the JAX-RS provider that this class represents a resource.

MovieOfTheDay.java Fragment: Class Declaration with @Path

/**
* Simple class that provides a movie for the provided month and day of that
* month.
*/
@Path("/movies")
public class MovieOfTheDay
{
// . . .
}


By specifying "/movies" as an argument to the @Path annotation, I am instructing the JAX-RS provider that this class represents a resource accessible in part by the URI portion "/movies".

With the @Path annotation provided on the class level, methods can be defined for the JAX-RS provider to tie to the uniform interface. The next code snippet demonstrates this for GET. There are actually two methods annotated with the @GET annotation. The first method, getMovieUsage(), simply returns a String specifying how the other GET method can be used. The other GET method is called getMovieOfTheDay(Integer,Integer) and it provides the movie of the day for the day provided as the two integer parameters (month and date in that month).

MovieOfTheDay.java Fragment: Two GET Methods

/**
* This method provides a "usage" string related to retrieving movie of the
* day information.
*
* @return String describing how to access movie of the day information.
*/
@GET
@Path("/")
@Produces("text/plain")
public String getMovieUsage()
{
return
"To see the movie of the day, provide URL with month and day: "
+ "\thttp://localhost:8080/rest/resources/movies/<<month>>/<<day>>";
}

/**
* Obtain the movie of the day as indicated by the provided month and date.
*
* @param month Month for which movie of the day is desired.
* @param date Date for which movie of the day is desired.
* @return Title of the movie of the day for provided month and date.
*/
@GET
@Path("/{month}/{date}")
@Consumes("text/plain")
@Produces("text/html")
public String getMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
final String movieOfTheDay = moviesOfTheMonth != null
? moviesOfTheMonth.get(date)
: "Fletch Lives!";
return movieOfTheDay != null
? generateSelectedMovieHtml(movieOfTheDay, month, date)
: generateSelectedMovieHtml("Fletch", month, date);
}


The @Path annotation was used on this entire class and then was again used in the previous code sample to annotate the two GET methods to add more to the overall path. In the first case, the usage method's path is defined simply as "/", meaning that it remains the same path as to the overall class. The second GET method, however, has an @Path annotation with "/month/date" specified as an argument. This means that the JAX-RS provider should match this method with a URI containing the class-level @Path's URI portion concatenated with this method's @Path's URI portion: movies/{month}/{date}. The curly braces indicate that the value is a placeholder. In this case, the {month} and {date} are placeholders for Integers (the types of the two parameters to the method that are annotated with @PathParam annotations that tie them directly to the placeholder names).

The code for the other methods on this HTTP-based uniform interface is similar @DELETE annotation makes it obvious which method is associated with deletion of a resource. In my case, I'm using the method associated with the @PUT annotation for both insert/create (new) behavior and for update/modify (change to existing) behavior. There has been controversy in the REST community about how closely (if at all) HTTP methods in HTTP-based REST applications should apply to the CRUD concept (Create/Read/Update/Delete) with the particularly contentious point being the relationship of HTTP methods PUT/POST to the CRUD operations of UPDATE/CREATE.

My current favorite treatment of the POST/PUT issue is John Calcote's blog post PUT or POST: The REST of the Story. For me, whether to use POST or PUT does come down to asking myself the question, "Is the modeled behavior idempotent?" If I answer "yes" (repeated calls always end up with same value/state), then I use PUT. If the modeled behavior is not idempotent (the same call leads to different results), then I use POST. As a side note, it is interesting that "Idempotence" was Eric Lippert's first covered word in his series Five-Dollar Words for Programmers.

Returning to the subject of JAX-RS and the uniform interface, here is a code snippet with the appropriate JAX-RS annotations for deleting and inserting/updating movies of the day.

MovieOfTheDay.java Fragment: DELETE and PUT Methods

/**
* Remove the movie of the day for the day whose month and date are provided
* as parameters.
*
* @param month Month of date for which movie is to be removed.
* @param date Date for which movie is to be removed.
* @return String representing movie of the day information was deleted.
*/
@DELETE
@Path("/{month}/{date}")
@Consumes("text/plain")
@Produces("text/html")
public String deleteMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
String movieRemoved = "";
if (moviesOfTheMonth != null)
{
movieRemoved = moviesOfTheMonth.remove(date);
}
return generateDeletedMovieHtml(movieRemoved);
}

/**
* 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);
}


The code shown to this point in this post illustrates use of JAX-RS annotations @GET, @DELETE, and @PUT to expose methods in the Java class as methods roughly equivalent respectively to reading/retrieving, deleting, and adding/updating. Before moving onto screen snapshots showing these simple examples in action, I provide the entire class (including the example snippets above and the helper utility methods called from the examples above) here in one listing for convenience.

MovieOfTheDay.java: The Complete Class

package rmoug.td2010.rest;

import java.util.Calendar;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.PUT;
import javax.ws.rs.Produces;

/**
* Simple class that provides a movie for the provided month and day of that
* month.
*/
@Path("/movies")
public class MovieOfTheDay
{
/** Handle to logger. */
private static final Logger LOGGER = Logger.getLogger("rmoug.td2010.rest.MovieOfTheDay");

/** Overall simulated database of movies of the day. */
private static final Map<Integer, Map<Integer, String>> MOVIE_OF_THE_DAY;

static
{
MOVIE_OF_THE_DAY = new ConcurrentHashMap<Integer, Map<Integer, String>>();

final Map<Integer, String> janMovies = new ConcurrentHashMap<Integer, String>();
janMovies.put(10, "Trading Places");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JANUARY), janMovies);

final Map<Integer, String> febMovies = new ConcurrentHashMap<Integer, String>();
febMovies.put(2, "Groundhog Day");
febMovies.put(13, "Casablanca");
febMovies.put(14, "Sleepless in Seattle");
febMovies.put(15, "How to Lose a Guy in 10 Days");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.FEBRUARY), febMovies);

final Map<Integer, String> marMovies = new ConcurrentHashMap<Integer, String>();
marMovies.put(16, "The Fugitive");
marMovies.put(17, "Darby O'Gill and the Little People");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.MARCH), marMovies);

final Map<Integer, String> aprMovies = new ConcurrentHashMap<Integer, String>();
aprMovies.put(25, "Raiders of the Lost Ark");
aprMovies.put(26, "Indiana Jones and the Temple of Doom");
aprMovies.put(27, "Indiana Jones and the Last Crusade");
aprMovies.put(28, "Indiana Jones and the Kingdom of the Crystal Skull");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.APRIL), aprMovies);

final Map<Integer, String> mayMovies = new ConcurrentHashMap<Integer, String>();
mayMovies.put(26, "Star Wars: Episode 1 - The Phantom Menace");
mayMovies.put(27, "Star Wars: Episode 2 - Attack of the Clones");
mayMovies.put(28, "Star Wars: Episode 3 - Revenge of the Sith");
mayMovies.put(29, "Star Wars: Episode 4 - A New Hope");
mayMovies.put(30, "Star Wars: Episode 5 - The Empire Strikes Back");
mayMovies.put(31, "Star Wars: Episode 6 - Return of the Jedi");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.MAY), mayMovies);

final Map<Integer, String> junMovies = new ConcurrentHashMap<Integer, String>();
junMovies.put(15, "The Great Outdoors");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JUNE), junMovies);

final Map<Integer, String> julMovies = new ConcurrentHashMap<Integer, String>();
julMovies.put(4, "Independence Day");
julMovies.put(21, "Men in Black");
julMovies.put(22, "Men in Black 2");
julMovies.put(23, "I, Robot");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.JULY), julMovies);

final Map<Integer, String> augMovies = new ConcurrentHashMap<Integer, String>();
augMovies.put(10, "North by Northwest");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.AUGUST), augMovies);

final Map<Integer, String> sepMovies = new ConcurrentHashMap<Integer, String>();
sepMovies.put(1, "Dead Poets Society");
sepMovies.put(20, "Uncle Buck");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.SEPTEMBER), sepMovies);

final Map<Integer, String> octMovies = new ConcurrentHashMap<Integer, String>();
octMovies.put(28, "Psycho");
octMovies.put(29, "Sixth Sense");
octMovies.put(30, "Ghostbusters");
octMovies.put(31, "Young Frankenstein");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.OCTOBER), octMovies);

final Map<Integer, String> novMovies = new ConcurrentHashMap<Integer, String>();
novMovies.put(10, "The Italian Job");
novMovies.put(11, "Ocean's Eleven");
novMovies.put(12, "Ocean's Twelve");
novMovies.put(13, "Ocean's Thirteen");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.NOVEMBER), novMovies);

final Map<Integer, String> decMovies = new ConcurrentHashMap<Integer, String>();
decMovies.put(1, "Holiday Inn");
decMovies.put(24, "It's A Wonderful Life");
decMovies.put(25, "A Christmas Carol");
decMovies.put(26, "A Christmas Story");
decMovies.put(27, "Home Alone");
MOVIE_OF_THE_DAY.put(Integer.valueOf(Calendar.DECEMBER), decMovies);
}

/**
* This method provides a "usage" string related to retrieving movie of the
* day information.
*
* @return String describing how to access movie of the day information.
*/
@GET
@Path("/")
@Produces("text/plain")
public String getMovieUsage()
{
return
"To see the movie of the day, provide URL with month and day: "
+ "\thttp://localhost:8080/rest/resources/movies/<<month>>/<<day>>";
}

/**
* Obtain the movie of the day as indicated by the provided month and date.
*
* @param month Month for which movie of the day is desired.
* @param date Date for which movie of the day is desired.
* @return Title of the movie of the day for provided month and date.
*/
@GET
@Path("/{month}/{date}")
@Consumes("text/plain")
@Produces("text/html")
public String getMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
final String movieOfTheDay = moviesOfTheMonth != null
? moviesOfTheMonth.get(date)
: "Fletch Lives!";
return movieOfTheDay != null
? generateSelectedMovieHtml(movieOfTheDay, month, date)
: generateSelectedMovieHtml("Fletch", month, date);
}

/**
* Remove the movie of the day for the day whose month and date are provided
* as parameters.
*
* @param month Month of date for which movie is to be removed.
* @param date Date for which movie is to be removed.
* @return String representing movie of the day information was deleted.
*/
@DELETE
@Path("/{month}/{date}")
@Consumes("text/plain")
@Produces("text/html")
public String deleteMovieOfTheDay(
@PathParam("month") final Integer month,
@PathParam("date") final Integer date)
{
final Map<Integer, String> moviesOfTheMonth =
MOVIE_OF_THE_DAY.get(month-1);
String movieRemoved = "";
if (moviesOfTheMonth != null)
{
movieRemoved = moviesOfTheMonth.remove(date);
}
return generateDeletedMovieHtml(movieRemoved);
}

/**
* 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);
}

/**
* Generate snippet of HTML related to selection of a movie.
*
* @param movieTitle Title of movie selected.
* @param movieMonth Month in which this was movie of the day.
* @param movieDay Day on which this was movie of the day.
* @return HTML describing selected movie of the day.
*/
private String generateSelectedMovieHtml(
final String movieTitle, final int movieMonth, final int movieDay)
{
final String movieOfTheDayBody =
"<p>The movie of the day for " + movieMonth + "/" + movieDay
+ " is '" + movieTitle + "'.</p>";
return generateHtmlTemplate("Movie of the Day", movieOfTheDayBody);
}

/**
* Generate snippet of HTML related to deletion of a movie.
*
* @param deletedMovieTitle Title of movie that was deleted.
* @return HTML describing movie that was deleted.
*/
private String generateDeletedMovieHtml(
final String deletedMovieTitle)
{
final String deletedMovieBody =
deletedMovieTitle != null
? "<p>The movie '" + deletedMovieTitle + "' was removed.</p>"
: "<p>No movie was deleted because no matching movie found.</p>";
return generateHtmlTemplate("Deleted Movie", deletedMovieBody);
}

/**
* Generate snippet of HTML related to added/updated movie information.
*
* @param addedMovieTitle Title of movie being added/updated.
* @return HTML describing added/updated movie.
*/
private String generateAddedMovieHtml(final String addedMovieTitle)
{
final String addedMovieBody =
addedMovieTitle != null
? "<p>The movie '" + addedMovieTitle + "' was added as the Movie of the Day."
: "<p>No movie added for movie of the day.</p>";
return generateHtmlTemplate("Added Movie", addedMovieBody);
}

/**
* Generic HTML template generated from provided HTML title and body.
*
* @param htmlTitle Title portion of HTML document to be generated.
* @param htmlBody Body portion of HTML document to be generated.
* @return HTML template populated with provided head and body.
*/
private String generateHtmlTemplate(
final String htmlTitle, final String htmlBody)
{
final StringBuilder builder = new StringBuilder();
builder.append("<html>")
.append("<head><title>")
.append(htmlTitle)
.append("</title></head>")
.append("<body><span style='font-weight: bold; font-size: 125%;")
.append("text-align: center;'>")
.append(htmlTitle)
.append("</span>");
builder.append(htmlBody);
builder.append("</body></html>");
return builder.toString();
}
}


In my introductory post on JAX-RS with GlassFish, I showed the web.xml file I used to deploy the JAX-RS-annotated class properly on GlassFish. That file is shown here again for convenience.

web.xml

?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<servlet>
<servlet-name>ServletAdaptor</servlet-name>
<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>ServletAdaptor</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

<session-config>
<session-timeout>
30
</session-timeout>
</session-config>

</web-app>


The previously referenced blog post provided relatively deep coverage of deploying the JAX-RS application to GlassFish v3, so I won't go into that again here. In that same post, I used a web browser as my simple REST client because I only used methods accessed via HTTP GET. In this post, however, I am also demonstrating DELETE and PUT, so I will use RESTClient. I have blogged previously on using RESTClient (in Easy Java-Based REST Testing Tools and in Using RESTClient with Java's HTTPServlet).

The first screen snapshot demonstrates running RESTClient with the method exposed via HTTP GET that provides usage information.



The next screen snapshot demonstrates the other GET-exposed method. This method provides an actual movie of the day.



In the previous two screen snapshots, the GET HTTP method was used as shown by the checked GET bullet. The next screen snapshot shows a movie of the day being added. In this case, the PUT radio bullet is checked on REST Client. The URL changes here to include a movie title.



I needed to specify the space in the movie title ("Mission Impossible") with %20 in this case. In a realistic, programmatic REST client, I'd make sure either the client framework or my own code ensured that spaces were translated for users rather than relying forcing the client to handle these %20 representations.

To call the deletion method, I simply need to change the HTTP method selected in the RESTClient GUI to "DELETE" and return to the URI with month and date integers. This is shown in the next screen snapshot.



If I run RESTClient GET with the same URI at this point, I'll get a message back saying that the movie of the day is Fletch because that is the default I use for any dates on which no movie of the day is assigned (and I had just deleted the movie assigned to that day).

JAX-RS supports REST in many more ways besides its making it easy to tie custom methods to specific HTTP methods, but I wanted to focus on how JAX-RS enables the uniform interface concept via the standard HTTP methods in this post. In future posts, I plan to consider how JAX-RS further helps with an HTTP-based REST application by handling other items such as content types and responses.