001 /*
002 * Copyright 2006 The Apache Software Foundation. Licensed under the Apache License, Version 2.0
003 * (the "License"); you may not use this file except in compliance with the License. You may obtain
004 * a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable
005 * law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
006 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
007 * for the specific language governing permissions and limitations under the License.
008 */
009
010 package org.apache.tapestry.util;
011
012 import java.text.ParseException;
013 import java.text.SimpleDateFormat;
014 import java.util.Date;
015 import java.util.Locale;
016 import java.util.Properties;
017 import java.util.TimeZone;
018
019 /**
020 * Converts dates to strings using the same format specifiers as strftime Note: This does not mimic
021 * strftime perfectly. Certain strftime commands, are not supported, and will convert as if they
022 * were literals. Certain complicated commands, like those dealing with the week of the year
023 * probably don't have exactly the same behavior as strftime. These limitations are due to use
024 * SimpleDateTime. If the conversion was done manually, all these limitations could be eliminated.
025 * The interface looks like a subset of DateFormat. Maybe someday someone will make this class
026 * extend DateFormat.
027 *
028 * <p>
029 * Added to tapestry in order to help with dojo/javascript date/time conversions.
030 * </p>
031 *
032 * @see "http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html"
033 * @author Bip Thelin
034 * @author Dan Sandberg
035 */
036 public class Strftime
037 {
038
039 protected static Properties translate;
040 protected static Properties pTranslate;
041 protected SimpleDateFormat simpleDateFormat;
042
043 /**
044 * Initialize our pattern translation
045 */
046 static {
047 translate = new Properties();
048 translate.put("a", "EEE");
049 translate.put("A", "EEEE");
050 translate.put("b", "MMM");
051 translate.put("B", "MMMM");
052 translate.put("c", "EEE MMM d HH:mm:ss yyyy");
053
054 // There's no way to specify the century in SimpleDateFormat. We don't want to hard-code
055 // 20 since this could be wrong for the pre-2000 files.
056 // translate.put("C", "20");
057 translate.put("d", "dd");
058 translate.put("D", "MM/dd/yy");
059 translate.put("e", "dd"); // will show as '03' instead of ' 3'
060 translate.put("F", "yyyy-MM-dd");
061 translate.put("g", "yy");
062 translate.put("G", "yyyy");
063 translate.put("H", "HH");
064 translate.put("h", "MMM");
065 translate.put("I", "hh");
066 translate.put("j", "DDD");
067 translate.put("k", "HH"); // will show as '07' instead of ' 7'
068 translate.put("l", "hh"); // will show as '07' instead of ' 7'
069 translate.put("m", "MM");
070 translate.put("M", "mm");
071 translate.put("n", "\n");
072 translate.put("p", "a");
073 translate.put("P", "a"); // will show as pm instead of PM
074 translate.put("r", "hh:mm:ss a");
075 translate.put("R", "HH:mm");
076 // There's no way to specify this with SimpleDateFormat
077 // translate.put("s","seconds since ecpoch");
078 translate.put("S", "ss");
079 translate.put("t", "\t");
080 translate.put("T", "HH:mm:ss");
081 // There's no way to specify this with SimpleDateFormat
082 // translate.put("u","day of week ( 1-7 )");
083
084 // There's no way to specify this with SimpleDateFormat
085 // translate.put("U","week in year with first sunday as first day...");
086
087 translate.put("V", "ww"); // I'm not sure this is always exactly the same
088
089 // There's no way to specify this with SimpleDateFormat
090 // translate.put("W","week in year with first monday as first day...");
091
092 // There's no way to specify this with SimpleDateFormat
093 // translate.put("w","E");
094 translate.put("X", "HH:mm:ss");
095 translate.put("x", "MM/dd/yy");
096 translate.put("y", "yy");
097 translate.put("Y", "yyyy");
098 translate.put("Z", "z");
099 translate.put("z", "Z");
100 translate.put("%", "%");
101
102 pTranslate = new Properties();
103 pTranslate.put("EEE", "%a");
104 pTranslate.put("EEEE", "%A");
105 pTranslate.put("MMM", "%b");
106 pTranslate.put("MMMM", "%B");
107 pTranslate.put("EEE MMM d HH:mm:ss yyyy", "%c");
108
109 // There's no way to specify the century in SimpleDateFormat. We don't want to hard-code
110 // 20 since this could be wrong for the pre-2000 files.
111 // translate.put("C", "20");
112 pTranslate.put("dd", "%d");
113 pTranslate.put("MM/dd/yy", "%D");
114 pTranslate.put("yyyy-MM-dd", "%F");
115 pTranslate.put("yy", "%g");
116 pTranslate.put("yyyy", "%G");
117 pTranslate.put("HH", "%H");
118 pTranslate.put("MMM", "%h");
119 pTranslate.put("hh", "%I");
120 pTranslate.put("DDD", "%j");
121 pTranslate.put("MM", "%m");
122 pTranslate.put("mm", "%M");
123 pTranslate.put("\n", "%n");
124 pTranslate.put("a", "%p"); // will show as pm instead of PM
125 pTranslate.put("hh:mm:ss a", "%r");
126 pTranslate.put("HH:mm", "%R");
127 // There's no way to specify this with SimpleDateFormat
128 // translate.put("s","seconds since ecpoch");
129 pTranslate.put("ss", "%S");
130 pTranslate.put("\t", "%t");
131 pTranslate.put("HH:mm:ss", "%T");
132 // There's no way to specify this with SimpleDateFormat
133 // translate.put("u","day of week ( 1-7 )");
134
135 // There's no way to specify this with SimpleDateFormat
136 // translate.put("U","week in year with first sunday as first day...");
137
138 pTranslate.put("ww", "%V"); // I'm not sure this is always exactly the same
139
140 // There's no way to specify this with SimpleDateFormat
141 // translate.put("W","week in year with first monday as first day...");
142
143 // There's no way to specify this with SimpleDateFormat
144 // translate.put("w","E");
145 pTranslate.put("HH:mm:ss", "%X");
146 pTranslate.put("MM/dd/yy", "%x");
147 pTranslate.put("yy", "%y");
148 pTranslate.put("yyyy", "%Y");
149 pTranslate.put("z", "%Z");
150 pTranslate.put("Z", "%z");
151 pTranslate.put("%", "%");
152 }
153
154 /**
155 * Create an instance of this date formatting class.
156 *
157 * @see #Strftime( String, Locale )
158 */
159 public Strftime(String origFormat)
160 {
161 String convertedFormat = convertDateFormat(origFormat);
162 simpleDateFormat = new SimpleDateFormat(convertedFormat);
163 }
164
165 /**
166 * Create an instance of this date formatting class.
167 *
168 * @param origFormat
169 * the strftime-style formatting string
170 * @param locale
171 * the locale to use for locale-specific conversions
172 */
173 public Strftime(String origFormat, Locale locale)
174 {
175 String convertedFormat = convertDateFormat(origFormat);
176 simpleDateFormat = new SimpleDateFormat(convertedFormat, locale);
177 }
178
179 /**
180 * Format the date according to the strftime-style string given in the constructor.
181 *
182 * @param date
183 * the date to format
184 * @return the formatted date
185 */
186 public String format(Date date)
187 {
188 return simpleDateFormat.format(date);
189 }
190
191 /**
192 * Parses the input.
193 *
194 * @see java.text.SimpleDateFormat#parse(String)
195 * @param input The string to parse.
196 * @return A parsed {@link Date}.
197 * @throws ParseException On input error.
198 */
199 public Date parse(String input)
200 throws ParseException
201 {
202 return simpleDateFormat.parse(input);
203 }
204
205 /**
206 * Get the timezone used for formatting conversions.
207 *
208 * @return the timezone
209 */
210 public TimeZone getTimeZone()
211 {
212 return simpleDateFormat.getTimeZone();
213 }
214
215 /**
216 * Change the timezone used to format dates.
217 *
218 * @see SimpleDateFormat#setTimeZone(TimeZone)
219 */
220 public void setTimeZone(TimeZone timeZone)
221 {
222 simpleDateFormat.setTimeZone(timeZone);
223 }
224
225 /**
226 * Does the exact opposite of {{@link #convertDateFormat(String)} by converting
227 * the incoming java date format string into a POSIX compliant format string.
228 * @param pattern The java date format style format
229 * @return The converted format into something usable by POSIX strftime style parser/formatters.
230 */
231 public static String convertToPosixFormat(String pattern)
232 {
233 if (pattern == null) return null;
234
235 StringBuffer buf = new StringBuffer();
236 int start=-1;
237
238 for(int i = 0; i < pattern.length(); i++) {
239 char c = pattern.charAt(i);
240
241 // if in a definition
242 if (Character.isLetter(c)) {
243 if (start <= -1) start = i;
244 continue;
245 } else if (start >= 0) {
246 // we've hit the end of a definition
247 String conv = pattern.substring(start, i);
248 String match = pTranslate.getProperty(conv);
249
250 if (match == null)
251 buf.append(conv); // just append it, this shouldn't happen we hope
252 else
253 buf.append(match);
254
255 // reset
256 start=-1;
257 }
258
259 buf.append(c);
260 }
261
262 // grab last one, if any
263 if (start > -1) {
264 String conv = pattern.substring(start, pattern.length());
265 String match = pTranslate.getProperty(conv);
266 if (match == null) buf.append(conv);
267 else buf.append(match);
268 }
269
270 return buf.toString();
271 }
272
273 /**
274 * Search the provided pattern and get the C standard Date/Time formatting rules and convert
275 * them to the Java equivalent.
276 *
277 * @param pattern
278 * The pattern to search
279 * @return The modified pattern
280 */
281 public static String convertDateFormat(String pattern)
282 {
283 boolean inside = false;
284 boolean mark = false;
285 boolean modifiedCommand = false;
286
287 StringBuffer buf = new StringBuffer();
288
289 for(int i = 0; i < pattern.length(); i++) {
290 char c = pattern.charAt(i);
291
292 if (c == '%' && !mark) {
293 mark = true;
294 } else {
295 if (mark) {
296 if (modifiedCommand) {
297 // don't do anything--we just wanted to skip a char
298 modifiedCommand = false;
299 mark = false;
300 } else {
301 inside = translateCommand(buf, pattern, i, inside);
302 // It's a modifier code
303 if (c == 'O' || c == 'E') {
304 modifiedCommand = true;
305 } else {
306 mark = false;
307 }
308 }
309 } else {
310 if (!inside && c != ' ') {
311 // We start a literal, which we need to quote
312 buf.append("'");
313 inside = true;
314 }
315
316 buf.append(c);
317 }
318 }
319 }
320
321 if (buf.length() > 0) {
322 char lastChar = buf.charAt(buf.length() - 1);
323
324 if (lastChar != '\'' && inside) {
325 buf.append('\'');
326 }
327 }
328 return buf.toString();
329 }
330
331 private static String quote(String str, boolean insideQuotes)
332 {
333 String retVal = str;
334 if (!insideQuotes) {
335 retVal = '\'' + retVal + '\'';
336 }
337 return retVal;
338 }
339
340 /**
341 * try to get the Java Date/Time formating associated with the C standard provided.
342 *
343 * @param c
344 * The C equivalent to translate
345 * @return The Java formatting rule to use
346 */
347 private static boolean translateCommand(StringBuffer buf, String pattern, int index,
348 boolean oldInside)
349 {
350 char firstChar = pattern.charAt(index);
351 boolean newInside = oldInside;
352
353 // O and E are modifiers, they mean to present an alternative representation of the next
354 // char
355 // we just handle the next char as if the O or E wasn't there
356 if (firstChar == 'O' || firstChar == 'E') {
357 if (index + 1 < pattern.length()) {
358 newInside = translateCommand(buf, pattern, index + 1, oldInside);
359 } else {
360 buf.append(quote("%" + firstChar, oldInside));
361 }
362 } else {
363 String command = translate.getProperty(String.valueOf(firstChar));
364
365 // If we don't find a format, treat it as a literal--That's what apache does
366 if (command == null) {
367 buf.append(quote("%" + firstChar, oldInside));
368 } else {
369 // If we were inside quotes, close the quotes
370 if (oldInside) {
371 buf.append('\'');
372 }
373 buf.append(command);
374 newInside = false;
375 }
376 }
377 return newInside;
378 }
379 }