001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010:
011: package org.mmbase.datatypes;
012:
013: import java.text.*;
014: import java.util.*;
015: import org.mmbase.util.LocalizedString;
016: import org.mmbase.util.DateFormats;
017: import org.mmbase.util.logging.Logging;
018: import org.mmbase.util.logging.Logger;
019:
020: /**
021: * This is a bit like SimpleDateFormat, because it accepts the same pattern String. It can also
022: * parse the String though (see {@link #getList}), which can be used to do something else
023: * for parsing or formatting (think: format an editor entry).
024: *
025: * This utility class is of course used in the implementation of {@link DateTimeDataType}.
026: *
027: * @author Michiel Meeuwissen
028: * @since MMBase-1.8
029: * @version $Id: DateTimePattern.java,v 1.15 2007/10/17 08:18:40 michiel Exp $
030: */
031:
032: public class DateTimePattern implements Cloneable, java.io.Serializable {
033: private static final Logger log = Logging
034: .getLoggerInstance(DateTimePattern.class);
035:
036: public static final DateTimePattern DEFAULT = new DateTimePattern(
037: "yyyy-MM-dd HH:mm:ss");
038:
039: private static final long serialVersionUID = 1L; // increase this if object serialization changes (which we shouldn't do!)
040:
041: protected LocalizedString pattern;
042:
043: public DateTimePattern(String pattern) {
044: this .pattern = new LocalizedString(pattern);
045: }
046:
047: public void set(String pattern, Locale locale) {
048: this .pattern.set(pattern, locale);
049: }
050:
051: public void set(String pattern) {
052: this .pattern.setKey(pattern);
053: }
054:
055: /**
056: * Returns a DateFormat object associated with this object.
057: */
058: public DateFormat getDateFormat(Locale locale) {
059: if (locale == null)
060: locale = LocalizedString.getDefault();
061: return DateFormats.getInstance(pattern.get(locale), null,
062: locale);
063: }
064:
065: /**
066: * Returns the original pattern, which can e.g. be used to instantiate a SimpleDateFormat (but this is also done for you in {@link #getDateFormat}.
067: */
068: public LocalizedString getPattern() {
069: return pattern;
070: }
071:
072: private static final char DONTAPPEND = (char) -1;
073:
074: private static List<String> parse(String p) {
075: List<String> parsed = new ArrayList<String>();
076: StringBuilder buf = new StringBuilder();
077: boolean inString = false;
078: boolean inQuote = false;
079:
080: char nonStringChar = (char) -1;
081: for (int i = 0; i < p.length(); i++) {
082: char c = p.charAt(i);
083: if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { // reserved
084: if (inString) {
085: if (!inQuote) {
086: if (buf.length() > 0) {
087: parsed.add(buf.toString());
088: buf.setLength(0);
089: }
090: inString = false;
091: nonStringChar = c;
092: }
093: } else {
094: if (nonStringChar != c) {
095: parsed.add(buf.toString());
096: buf.setLength(0);
097: nonStringChar = c;
098: }
099: }
100: } else {
101: if (!inString) {
102: if (i != 0) {
103: parsed.add(buf.toString());
104: buf.setLength(0);
105: }
106: buf.append("\'");
107: inQuote = false;
108: inString = true;
109: }
110: if (inString) {
111: if (c == '\'') {
112: if (inQuote && i > 0 && p.charAt(i - 1) == '\'') {
113: // enabling escape of '.
114: } else {
115: c = DONTAPPEND;
116: }
117: inQuote = !inQuote;
118:
119: }
120: }
121:
122: }
123: if (c != DONTAPPEND) {
124: buf.append(c);
125: }
126: }
127: if (inQuote) {
128: throw new IllegalArgumentException("Unterminated quote");
129: }
130: if (buf.length() > 0) {
131: parsed.add(buf.toString());
132: }
133: return parsed;
134: }
135:
136: /**
137: * Returns the pattern 'parsed'. This means that is is a List of Strings. If the string is
138: * introduced by a quote, then it is a literal string, otherwise it is a format-string,
139: * consisting only of a number of the same letters (e.g. yyy). So by checking the first
140: * character you can decide what to do with it. If for example you are making an editor, and
141: * the first char is an quote, you may decide to do either nothing, or to write it out (without
142: * the quote). If the first character is e.g. 'y' you can make an input box for the year (you
143: * could also attribute some meaning to the length of the string then).
144: *
145: *
146: */
147: public List<String> getList(Locale locale) {
148: String p = pattern.get(locale);
149: return parse(p);
150: }
151:
152: private static final Element HOUR_OF_DAY = new Element("hour",
153: Calendar.HOUR_OF_DAY, 0, 23);
154: private static final Element HOUR = new Element("hourinampm",
155: Calendar.HOUR, 0, 11);
156: private static final Element HOUR_OF_DAY_1 = new Element("hour",
157: Calendar.HOUR_OF_DAY, 1, 24, 1);
158: private static final Element HOUR_1 = new Element("hour",
159: Calendar.HOUR, 1, 12, 1);
160: private static final Element MINUTE = new Element("minute",
161: Calendar.MINUTE, 0, 59);
162: private static final Element SECOND = new Element("second",
163: Calendar.SECOND, 0, 59);
164: private static final Element DAY_OF_WEEK = new Element("dayofweek",
165: Calendar.DAY_OF_WEEK, 1, 7) {
166: public String toString(int value, Locale locale, int length) {
167: SimpleDateFormat format = new SimpleDateFormat(
168: "EEEEEEEEEEEEE".substring(0, length), locale);
169: Calendar help = Calendar.getInstance();
170: help.clear();
171: help.set(field, value);
172: return format.format(help.getTime());
173: }
174: };
175: private static final Element WEEK_OF_YEAR = new Element(
176: "weekinyear", Calendar.WEEK_OF_YEAR, 1, 53);
177: private static final Element DAY_OF_YEAR = new Element("dayinyear",
178: Calendar.DAY_OF_YEAR, 1, 366);
179: private static final Element DAY_OF_WEEK_IN_MONTH = new Element(
180: "dayofweekinmonth", Calendar.DAY_OF_WEEK_IN_MONTH, 1, 5) {
181: public String toString(int value, Locale locale, int length) {
182: if (length > 0) {
183: ResourceBundle bundle = ResourceBundle.getBundle(
184: "org.mmbase.datatypes.resources.ordinals",
185: locale);
186: return bundle.getString("" + value);
187: } else {
188: return super .toString(value, locale, length);
189: }
190: }
191: };
192:
193: private static final Element AM_PM = new Element("am_pm",
194: Calendar.AM_PM, 0, //AM
195: 1 //PM
196: ) {
197: public String toString(int value, Locale locale, int length) {
198: SimpleDateFormat format = new SimpleDateFormat(
199: "aaaaaaaaaaaaaa".substring(0, length), locale);
200: Calendar help = Calendar.getInstance();
201: help.clear();
202: help.set(field, value);
203: return format.format(help.getTime());
204: }
205: };
206: private static final Element MILLISECOND = new Element(
207: "millisecond", Calendar.MILLISECOND, 0, 999);
208:
209: /**
210: * Returns an {@link Element} structure assiocated with the characters of the format
211: * pattern. This utility function can be usefull when generating drop-downs based on the result
212: * of {@link #getList}.
213: * @param c The pattern character. 'y', 'M', 'd', 'H', 'K', 'h', 'k', 'm', 's', 'E', 'w', 'D', 'F', 'G', 'a', or 'S'. Also u is recognized (as in the ICU version of SimpleDateFormat), for years which can also be negative (targeted at GregorianCalendar with 2 era's)
214: * @param minDate If for example the parameter is 'y' then the 'getMin' property of the result
215: * Element will be the year of this date.
216: * @param maxDate If for example the parameter is 'y' then the 'getMax' property of the result
217: * Element will be the year of this date.
218: */
219: public static Element getElement(final char c, Calendar minDate,
220: Calendar maxDate) {
221: switch (c) {
222: case 'G': {
223: int startEra = minDate.get(Calendar.ERA);
224: int endEra = maxDate.get(Calendar.ERA);
225: return new Element("era", Calendar.ERA, startEra, //BC
226: endEra //AD
227: ) {
228: public String toString(int value, Locale locale,
229: int length) {
230: SimpleDateFormat format = new SimpleDateFormat(
231: "GGGGGGGGGGGG".substring(0, length), locale);
232: Calendar help = Calendar.getInstance();
233: help.clear();
234: help.set(field, value);
235: return format.format(help.getTime());
236: }
237: };
238: }
239: case 'y': {
240: int startEra = minDate.get(Calendar.ERA);
241: int endEra = maxDate.get(Calendar.ERA);
242: int startYear = minDate.get(Calendar.YEAR);
243: int endYear = maxDate.get(Calendar.YEAR);
244: if (startEra < endEra) { // you'll need an ERA indicator too, if this happens, and you want to be able to enter dates BC.
245: endYear = endYear > startYear ? endYear : startYear;
246: startYear = minDate.getActualMinimum(Calendar.YEAR);
247: }
248: return new Element("year", Calendar.YEAR, startYear,
249: endYear) {
250: public int getNullValue() {
251: return Integer.MAX_VALUE;
252: }
253: };
254: }
255: case 'u': { // this is not a SimpleDateFormat character (it is a com.ibm.icu.text.SimpleDateFormat compatible though)
256: int startEra = minDate.get(Calendar.ERA);
257: int endEra = maxDate.get(Calendar.ERA);
258: int startYear = minDate.get(Calendar.YEAR);
259: if (minDate instanceof GregorianCalendar
260: && startEra == GregorianCalendar.BC)
261: startYear = -1 * (startYear - 1);
262: int endYear = maxDate.get(Calendar.YEAR);
263: if (maxDate instanceof GregorianCalendar
264: && endEra == GregorianCalendar.BC)
265: endYear = -1 * (endYear - 1);
266: return new Element("year", Calendar.YEAR, startYear,
267: endYear) {
268: public int getValue(Calendar cal) {
269: int era = cal.get(Calendar.ERA);
270: int year = cal.get(Calendar.YEAR);
271: if (cal instanceof GregorianCalendar
272: && era == GregorianCalendar.BC)
273: year = -1 * (year - 1);
274: return year;
275: }
276:
277: public int getNullValue() {
278: return Integer.MAX_VALUE;
279: }
280: };
281: }
282: case 'M': {
283: int startYear = minDate.get(Calendar.YEAR);
284: int endYear = maxDate.get(Calendar.YEAR);
285: int min, max;
286: if (startYear == endYear) {
287: min = minDate.get(Calendar.MONTH) + 1;
288: max = maxDate.get(Calendar.MONTH) + 1;
289: } else {
290: min = 1;
291: max = 12;
292: }
293: return new Element("month", Calendar.MONTH, min, max, 1) {
294: public String toString(int value, Locale locale,
295: int length) {
296: SimpleDateFormat format = new SimpleDateFormat(
297: "MMMMMMMMMMMMMMMMMM".substring(0, length),
298: locale);
299: Calendar help = Calendar.getInstance();
300: help.clear();
301: help.set(field, value);
302: return format.format(help.getTime());
303: }
304: };
305: }
306: case 'd': {
307: int startYear = minDate.get(Calendar.YEAR);
308: int endYear = maxDate.get(Calendar.YEAR);
309: int min, max;
310: if (startYear == endYear) {
311: int minMonth = minDate.get(Calendar.MONTH) + 1;
312: int maxMonth = maxDate.get(Calendar.MONTH) + 1;
313: if (minMonth == maxMonth) {
314: min = minDate.get(Calendar.DAY_OF_MONTH);
315: max = maxDate.get(Calendar.DAY_OF_MONTH);
316: } else {
317: min = 1;
318: max = 31;
319: }
320: } else {
321: min = 1;
322: max = 31;
323: }
324: return new Element("day", Calendar.DAY_OF_MONTH, min, max);
325: }
326: // ignore minDate, maxDate for these, never mind..
327: case 'H':
328: return HOUR_OF_DAY;
329: case 'K':
330: return HOUR;
331: case 'h':
332: return HOUR_OF_DAY_1;
333: case 'k':
334: return HOUR_1;
335: case 'm':
336: return MINUTE;
337: case 's':
338: return SECOND;
339: case 'E':
340: return DAY_OF_WEEK;
341: case 'w':
342: return WEEK_OF_YEAR;
343: case 'D':
344: return DAY_OF_YEAR;
345: case 'F':
346: return DAY_OF_WEEK_IN_MONTH;
347: case 'a':
348: return AM_PM;
349: case 'S':
350: return MILLISECOND;
351: default:
352: log.warn("Unknown pattern " + c);
353: return null;
354: }
355: }
356:
357: /**
358: * A wrapper arround a field in a {@link java.util.Calendar} object. It provides a
359: * minimal and maximal value for the integer value, which can be requested by code which is
360: * producing a user interface to enter dates.
361: */
362: public static class Element {
363: private final String name;
364: final int field;
365: private final int min;
366: private final int max;
367: private final int offset;
368:
369: Element(String n, int field, int min, int max) {
370: this (n, field, min, max, 0);
371: }
372:
373: Element(String n, int field, int min, int max, int offset) {
374: name = n;
375: this .field = field;
376: this .min = min;
377: this .max = max;
378: this .offset = offset;
379: }
380:
381: /**
382: * The name of the field in a Calendar object. Like e.g. 'day' or 'second'.
383: */
384: public final String getName() {
385: return name;
386: }
387:
388: /**
389: * The associated constant in {@link java.util.Calendar}, e.g. {@link
390: * java.util.Calendar.DAY_OF_MONTH} or {@link java.util.Calendar#SECOND}
391: */
392: public final int getField() {
393: return field;
394: }
395:
396: /**
397: * The minimal value this field of the Calendar object can take.
398: */
399: public final int getMin() {
400: return min;
401: }
402:
403: /**
404: * The maximal value this field of the Calendar object can take.
405: */
406: public final int getMax() {
407: return max;
408: }
409:
410: /**
411: * An offset to be used for presentation. E.g. months are represented by number from 0 to 11
412: * in Calendar objects but you typically want to present 1 to 12, so the offset is 1 then.
413: */
414: public final int getOffset() {
415: return offset;
416: }
417:
418: /**
419: * Normally equivalent with <code>cal.getValue(getField())</code>
420: * @return The value for this element for a certain Calendar instance
421: */
422: public int getValue(Calendar cal) {
423: return cal.get(getField());
424: }
425:
426: /**
427: * Converts a value for the Calendar field associated with this Element to a
428: * String. Typically used when creating optionlists.
429: * @param value the value to convert
430: * @param locale A locale can be used in some instances. E.g. to generate month names.
431: * @param length An indication of verboseness. Typically numeric results if a small number
432: * (perhaps filled to this length) or words if a big number (and it makes sense, e.g. for
433: * months, and weekdays).
434: */
435: public String toString(int value, Locale locale, int length) {
436: StringBuilder buf = new StringBuilder("" + value);
437: while (buf.length() < length) {
438: buf.insert(0, "0");
439: }
440: return buf.toString();
441: }
442:
443: public String toString() {
444: return getName() + " [" + min + ", " + max + "] + "
445: + getOffset();
446: }
447:
448: /**
449: * The int-value representing <code>null</code>. Some otherwise impossible value for the
450: * field. This can be use as a marker value in the option-list to set the calendar value to <code>null</code>.
451: */
452: public int getNullValue() {
453: return -1;
454: }
455: }
456:
457: public Object clone() {
458: try {
459: DateTimePattern clone = (DateTimePattern) super .clone();
460: clone.pattern = (LocalizedString) pattern.clone();
461: return clone;
462: } catch (CloneNotSupportedException cns) {
463: // should not happen
464: throw new RuntimeException(cns);
465: }
466: }
467:
468: public String toString() {
469: return pattern.toString();
470: }
471:
472: public static void main(String argv[]) {
473: String input;
474: if (argv.length > 0) {
475: input = argv[0];
476: } else {
477: input = "yyyy-MM-dd";
478: }
479: DateTimePattern df = new DateTimePattern(input);
480: df.set("yyyy-MM-dd", Locale.FRANCE);
481:
482: DateTimePattern df2 = (DateTimePattern) df.clone();
483: df2.set("HH:mm:ss");
484: df2.set("yyyy;MM;dd", Locale.FRANCE);
485:
486: System.out.println("" + df.getList(Locale.FRANCE));
487: System.out.println("" + df2.getList(Locale.FRANCE));
488:
489: }
490:
491: }
|