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    }