Java tutorial
package org.novoj.utils.datePattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.*; import java.util.regex.Pattern; /** * This utility class allows you to decompose Java date / time conversion patterns and allows to convert it into the * jQueryUI DatePicker and TimePicker format. Also contains logic for clever Date parsing when user doesn't enter the * date in the precise requested format. * * @author Jan Novotn, Licensed under the LGPL */ public class DatePatternConverter { private static final Log log = LogFactory.getLog(DatePatternConverter.class); private static final Map<String, String> java2jQueryUIdatePatterns = new HashMap<String, String>(); private static final Map<String, String> java2jQueryUItimePatterns = new HashMap<String, String>(); private static final Set<Character> specialCharacters = new HashSet<Character>(); private static final List<DateFormatHolder> dateRecognitionPatterns = new ArrayList<DateFormatHolder>(); private static final List<DateFormatHolder> timeRecognitionPatterns = new ArrayList<DateFormatHolder>(); private static final List<DateFormatHolder> dateTimeRecognitionPatterns = new ArrayList<DateFormatHolder>(); private static final Pattern REGEX_APOSTROPHE = Pattern.compile("\'"); /** @noinspection SimpleDateFormatWithoutLocale*/ static { java2jQueryUIdatePatterns.put("yyyy", "yy"); java2jQueryUIdatePatterns.put("yy", "y"); java2jQueryUIdatePatterns.put("MMMMM", "MM"); java2jQueryUIdatePatterns.put("MMM", "M"); java2jQueryUIdatePatterns.put("MM", "mm"); java2jQueryUIdatePatterns.put("M", "m"); java2jQueryUIdatePatterns.put("D", "o"); java2jQueryUIdatePatterns.put("dd", "dd"); java2jQueryUIdatePatterns.put("d", "d"); java2jQueryUIdatePatterns.put("EEEE", "DD"); java2jQueryUIdatePatterns.put("EEE", "D"); java2jQueryUIdatePatterns.put("EE", "D"); java2jQueryUIdatePatterns.put("E", "D"); java2jQueryUItimePatterns.put("HH", "hh"); java2jQueryUItimePatterns.put("H", "h"); java2jQueryUItimePatterns.put("hh", "hh"); java2jQueryUItimePatterns.put("h", "h"); java2jQueryUItimePatterns.put("mm", "mm"); java2jQueryUItimePatterns.put("m", "m"); java2jQueryUItimePatterns.put("ss", "ss"); java2jQueryUItimePatterns.put("s", "s"); java2jQueryUItimePatterns.put("a", "TT"); specialCharacters.add('.'); specialCharacters.add(':'); specialCharacters.add('-'); specialCharacters.add('/'); specialCharacters.add('\\'); specialCharacters.add(','); //make cartesian list of possible date combinations dateRecognitionPatterns.add(getDateFormat("yyyy", false)); dateRecognitionPatterns.add(getDateFormat("d", false)); dateRecognitionPatterns.add(getDateFormat("dd", false)); dateRecognitionPatterns.add(getDateFormat("MMMM", true)); dateRecognitionPatterns.add(getDateFormat("d.M", true)); dateRecognitionPatterns.add(getDateFormat("dd.MM", true)); dateRecognitionPatterns.add(getDateFormat("d.MM", true)); dateRecognitionPatterns.add(getDateFormat("dd.M", true)); dateRecognitionPatterns.add(getDateFormat("d.M.", true)); dateRecognitionPatterns.add(getDateFormat("dd.MM.", true)); dateRecognitionPatterns.add(getDateFormat("d.MM.", true)); dateRecognitionPatterns.add(getDateFormat("dd.M.", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.M.d", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.M.dd", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MM.d", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MMM.d", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MMMM.d", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MM.dd", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MMM.dd", true)); dateRecognitionPatterns.add(getDateFormat("yyyy.MMMM.dd", true)); dateRecognitionPatterns.add(getDateFormat("d.M.yy", true)); dateRecognitionPatterns.add(getDateFormat("dd.M.yy", true)); dateRecognitionPatterns.add(getDateFormat("d.MM.yy", true)); dateRecognitionPatterns.add(getDateFormat("d.MMM.yy", true)); dateRecognitionPatterns.add(getDateFormat("d.MMMM.yy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MM.yy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MMM.yy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MMMM.yy", true)); dateRecognitionPatterns.add(getDateFormat("d.M.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("dd.M.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("d.MM.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("d.MMM.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("d.MMMM.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MM.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MMM.yyyy", true)); dateRecognitionPatterns.add(getDateFormat("dd.MMMM.yyyy", true)); Collections.reverse(dateRecognitionPatterns); timeRecognitionPatterns.add(getDateFormat("H", false)); timeRecognitionPatterns.add(getDateFormat("HH", false)); timeRecognitionPatterns.add(getDateFormat("H.m", true)); timeRecognitionPatterns.add(getDateFormat("HH.m", true)); timeRecognitionPatterns.add(getDateFormat("H.mm", true)); timeRecognitionPatterns.add(getDateFormat("HH.mm", true)); timeRecognitionPatterns.add(getDateFormat("h.a", true)); timeRecognitionPatterns.add(getDateFormat("hh.a", true)); timeRecognitionPatterns.add(getDateFormat("h.m.a", true)); timeRecognitionPatterns.add(getDateFormat("hh.m.a", true)); timeRecognitionPatterns.add(getDateFormat("hh.mm.a", true)); timeRecognitionPatterns.add(getDateFormat("h.mm.a", true)); timeRecognitionPatterns.add(getDateFormat("H.m.s", true)); timeRecognitionPatterns.add(getDateFormat("HH.m.s", true)); timeRecognitionPatterns.add(getDateFormat("H.mm.s", true)); timeRecognitionPatterns.add(getDateFormat("HH.mm.s", true)); timeRecognitionPatterns.add(getDateFormat("H.m.ss", true)); timeRecognitionPatterns.add(getDateFormat("HH.m.ss", true)); timeRecognitionPatterns.add(getDateFormat("H.mm.ss", true)); timeRecognitionPatterns.add(getDateFormat("H.m.s.S", true)); timeRecognitionPatterns.add(getDateFormat("HH.m.s.S", true)); timeRecognitionPatterns.add(getDateFormat("H.mm.s.S", true)); timeRecognitionPatterns.add(getDateFormat("HH.mm.s.S", true)); timeRecognitionPatterns.add(getDateFormat("H.m.ss.S", true)); timeRecognitionPatterns.add(getDateFormat("HH.m.ss.S", true)); timeRecognitionPatterns.add(getDateFormat("H.mm.ss.S", true)); timeRecognitionPatterns.add(getDateFormat("HH.mm.ss.S", true)); Collections.reverse(timeRecognitionPatterns); for (DateFormatHolder dateFormat : dateRecognitionPatterns) { if (dateFormat.isCombinable()) { for (DateFormatHolder timeFormat : timeRecognitionPatterns) { if (timeFormat.isCombinable()) { dateTimeRecognitionPatterns .add(getDateFormat(dateFormat.getPattern() + "." + timeFormat.getPattern(), false)); } } } } } /** * Construct new DateFormatHolder. * @param pattern * @param combinable * @return */ private static DateFormatHolder getDateFormat(String pattern, boolean combinable) { return new DateFormatHolder(pattern, combinable); } /** * Tries to recognize date / time pattern from arbitrary string. Uses several decomposition patterns to recognize * proper value. It tries to make up missing parts in the natural way (current or initial date / time). * * @param dateAsString - user entered string * @param dateRequested - true if you expect some date value * @param timeRequested - true if you expect some time value * @param locale - locale used to parse string (passed to the SimpleDateFormat object) * @return */ @SuppressWarnings({ "OverlyComplexMethod" }) public Date getDate(String dateAsString, boolean dateRequested, boolean timeRequested, Locale locale) { String normalizedDateValue = getNormalizedDateValue(dateAsString, locale); Date now = new Date(); GregorianCalendar nowCld = new GregorianCalendar(); nowCld.setTime(now); if (dateRequested && timeRequested) { for (DateFormatHolder dateFormat : dateTimeRecognitionPatterns) { Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, false); if (parsedDate != null) { return parsedDate; } } for (DateFormatHolder dateFormat : dateRecognitionPatterns) { Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, false); if (parsedDate != null) { return parsedDate; } } for (DateFormatHolder timeFormat : timeRecognitionPatterns) { Date parsedDate = tryToParse(normalizedDateValue, timeFormat, nowCld, locale, false); if (parsedDate != null) { return parsedDate; } } } else if (dateRequested) { for (DateFormatHolder dateFormat : dateRecognitionPatterns) { Date parsedDate = tryToParse(normalizedDateValue, dateFormat, nowCld, locale, true); if (parsedDate != null) { return parsedDate; } } } else if (timeRequested) { for (DateFormatHolder timeFormat : timeRecognitionPatterns) { Date parsedDate = tryToParse(normalizedDateValue, timeFormat, nowCld, locale, true); if (parsedDate != null) { return parsedDate; } } } return null; } /** * This method normalizes entered date to the least valid form: * - white spaces are removed unless they delimit two valid values * - more consequential whitespaces are compressed to one character * - whitespace characters are converted to space character (32) * - special characters swallow border whitespaces * - more consequential special characters are compressed to one character * - string is trimmed * - special characters at the beginning of the string are ignored * - string is lowercased * - all special characters are converted to dot character * * @param value * @return */ @SuppressWarnings({ "OverlyComplexMethod" }) public String getNormalizedDateValue(String value, Locale locale) { String firstNormalization = value.trim().toLowerCase(locale); StringBuilder result = new StringBuilder(); boolean whitespace = false; boolean specialCharacter = false; for (int i = 0; i < firstNormalization.length(); i++) { char letter = firstNormalization.charAt(i); if (specialCharacters.contains(letter)) { specialCharacter = true; } else if (Character.isDigit(letter) || Character.isLetter(letter)) { if (result.length() > 0) { if (specialCharacter || whitespace) { result.append('.'); } } whitespace = false; specialCharacter = false; result.append(letter); } else if (Character.isWhitespace(letter)) { whitespace = true; } } if (specialCharacter) { result.append("."); } return result.toString(); } /** * This method decomposes original pattern into the recognizable chunks. Chunks are composed of: * - same letters * - parts escaped with apostrophe (anything could be inside) * - unknown characters whose are not letters (comma, dot, double dot, space and so on) * * @param pattern * @return */ @SuppressWarnings({ "OverlyLongMethod", "ImplicitNumericConversion" }) public List<String> getPatternDecomposition(String pattern) { List<String> patternDecomposition = new ArrayList<String>(); boolean escaped = false; char lastLetter = (char) 0; StringBuilder buffer = new StringBuilder(); for (int i = 0; i < pattern.length(); i++) { char letter = pattern.charAt(i); if (letter == '\'') { if (escaped) { if (pattern.charAt(i + 1) == '\'') { i++; buffer.append("\'\'"); } else { escaped = false; buffer.append('\''); buffer = resetBuffer(patternDecomposition, buffer); } } else { buffer = resetBuffer(patternDecomposition, buffer); escaped = true; buffer.append('\''); } } else if (escaped) { buffer.append(letter); } else if (Character.isLetter(letter)) { if (letter == lastLetter || lastLetter == (char) 0) { buffer.append(letter); } else { buffer = resetBuffer(patternDecomposition, buffer); buffer.append(letter); } } else if (specialCharacters.contains(new Character(letter))) { buffer = resetBuffer(patternDecomposition, buffer); patternDecomposition.add(String.valueOf(letter)); } else { if (buffer.length() > 0 && Character.isLetter(buffer.charAt(0))) { buffer = resetBuffer(patternDecomposition, buffer); } buffer.append(letter); } lastLetter = letter; } resetBuffer(patternDecomposition, buffer); return patternDecomposition; } /** * Returns true if date time pattern contains any sensible time part. * @param patternDecomposition * @return */ public boolean hasTime(List<String> patternDecomposition) { for (String patternPart : patternDecomposition) { if (java2jQueryUItimePatterns.containsKey(patternPart)) { return true; } } return false; } /** * Returns true if date time pattern contains any sensible date part. * @param patternDecomposition * @return */ public boolean hasDate(List<String> patternDecomposition) { for (String patternPart : patternDecomposition) { if (java2jQueryUIdatePatterns.containsKey(patternPart)) { return true; } } return false; } /** * Converts Java DateFormat pattern to the jQueryUI DatePicker Javascript format (or throws PatternNotConvertableException). * @param patternDecomposition * @return * @throws PatternNotConvertableException */ public String javaToJavascriptDatePatternConversion(List<String> patternDecomposition) throws PatternNotConvertableException { return conversion(patternDecomposition, java2jQueryUIdatePatterns, java2jQueryUItimePatterns, true); } /** * Converts Java DateFormat pattern to the jQueryUI DatePicker TimePicker addon Javascript format (or throws * PatternNotConvertableException). * @param patternDecomposition * @return * @throws PatternNotConvertableException */ public String javaToJavascriptTimePatternConversion(List<String> patternDecomposition) throws PatternNotConvertableException { return conversion(patternDecomposition, java2jQueryUItimePatterns, java2jQueryUIdatePatterns, false); } /** * Returns delimiter between date and time part of the DateFormat pattern. This part is needed to by handed forward * to the jQueryUI DatePicker to combine data with TimePicker addon. * * @param patternDecomposition * @return * @throws PatternNotConvertableException */ @SuppressWarnings({ "ImplicitNumericConversion", "OverlyComplexMethod" }) public String getDateAndTimeDelimiter(List<String> patternDecomposition) throws PatternNotConvertableException { boolean anyPatternRecognized = false; Map<String, String> patternSource = null; StringBuffer result = new StringBuffer(); for (String patternPart : patternDecomposition) { char leadLetter = patternPart.charAt(0); if (leadLetter == '\'') { if (anyPatternRecognized) { result.append(patternPart); } } else if (Character.isLetter(leadLetter)) { if (java2jQueryUIdatePatterns.containsKey(patternPart) || java2jQueryUItimePatterns.containsKey(patternPart)) { if (patternSource != null && patternSource.containsKey(patternPart)) { result = new StringBuffer(); } else { if (anyPatternRecognized) { break; } else { anyPatternRecognized = true; patternSource = java2jQueryUIdatePatterns.containsKey(patternPart) ? java2jQueryUIdatePatterns : java2jQueryUItimePatterns; } } } } else { if (anyPatternRecognized) { if (result.length() > 0 || !specialCharacters.contains(new Character(patternPart.charAt(0)))) { result.append(patternPart); } } } } return REGEX_APOSTROPHE.matcher(result.toString()).replaceAll(""); } /** * Tryes to parse string pattern into the date and returns non null value when succeeded. Otherwise returns null * (never throws exception). */ private static Date tryToParse(String normalizedDateValue, DateFormatHolder dateFormat, Calendar nowCld, Locale locale, boolean resetTime) { try { if (log.isTraceEnabled()) { log.trace("Trying format: " + dateFormat.getPattern() + " (" + dateFormat.getFormat(locale).format(new Date()) + ")"); } Date parsedDate = dateFormat.getFormat(locale).parse(normalizedDateValue); if (normalizedDateValue.equals(dateFormat.getFormat(locale).format(parsedDate))) { parsedDate = normalizeDate(nowCld, parsedDate, locale, dateFormat.getPattern()); parsedDate = normalizeHours(nowCld, parsedDate, locale, dateFormat.getPattern(), resetTime); return parsedDate; } else { //Format cannot be used - reverse formatting did not produce exactly same string! return null; } } catch (ParseException ignored) { //continue - this pattern wasn't just right return null; } } /** * Method returns date object with normalized date: * - when pattern doesn't contain year pattern -> current year is used * - when pattern doesn't contain month pattern * date represents current year -> current month is used * else -> january is used * - when pattern doesn't contain day pattern * date represents current year and month -> current day is used * else -> first day of month is used */ private static Date normalizeDate(Calendar nowCld, Date date, Locale locale, String pattern) { GregorianCalendar dateCld = new GregorianCalendar(); dateCld.setTime(date); //set current date if (pattern.toLowerCase(locale).indexOf('y') == -1) { dateCld.set(Calendar.YEAR, nowCld.get(Calendar.YEAR)); } if (pattern.indexOf('M') == -1) { if (nowCld.get(Calendar.YEAR) == dateCld.get(Calendar.YEAR)) { dateCld.set(Calendar.MONTH, nowCld.get(Calendar.MONTH)); } else { dateCld.set(Calendar.MONTH, 0); } } if (pattern.toLowerCase(locale).indexOf('d') == -1) { if (nowCld.get(Calendar.YEAR) != dateCld.get(Calendar.YEAR) || nowCld.get(Calendar.MONTH) != dateCld.get(Calendar.MONTH)) { dateCld.set(Calendar.DAY_OF_MONTH, 1); } else { dateCld.set(Calendar.DAY_OF_MONTH, nowCld.get(Calendar.DAY_OF_MONTH)); } } return dateCld.getTime(); } /** * Method returns date object with normalized time. When resetTime = true, time will be set to 0:0:0.0. * When date respresents current day (current date) and pattern doesn't contain appropriate pattern parts for * hour, minute, second, milliseconds - those values will be set to current time values, otherwise they will be * set to initial values (ie. 0). */ private static Date normalizeHours(Calendar nowCld, Date date, Locale locale, String pattern, boolean resetTime) { GregorianCalendar dateCld = new GregorianCalendar(); dateCld.setTime(date); boolean currentYear = nowCld.get(Calendar.YEAR) == dateCld.get(Calendar.YEAR); boolean currentMonth = nowCld.get(Calendar.MONTH) == dateCld.get(Calendar.MONTH); boolean currentDay = nowCld.get(Calendar.DAY_OF_MONTH) == dateCld.get(Calendar.DAY_OF_MONTH); //noinspection OverlyComplexBooleanExpression if (!resetTime && currentYear && currentMonth && currentDay) { //set current time if (pattern.toLowerCase(locale).indexOf('h') == -1) { dateCld.set(Calendar.HOUR, nowCld.get(Calendar.HOUR)); } if (pattern.indexOf('m') == -1) { dateCld.set(Calendar.MINUTE, nowCld.get(Calendar.MINUTE)); } if (pattern.indexOf('s') == -1) { dateCld.set(Calendar.SECOND, nowCld.get(Calendar.SECOND)); } if (pattern.indexOf('S') == -1) { dateCld.set(Calendar.MILLISECOND, nowCld.get(Calendar.MILLISECOND)); } } else { //set zero time if (pattern.toLowerCase(locale).indexOf('h') == -1) { dateCld.set(Calendar.HOUR, 0); } if (pattern.indexOf('m') == -1) { dateCld.set(Calendar.MINUTE, 0); } if (pattern.indexOf('s') == -1) { dateCld.set(Calendar.SECOND, 0); } if (pattern.indexOf('S') == -1) { dateCld.set(Calendar.MILLISECOND, 0); } } return dateCld.getTime(); } /** * Converts Java DateFormat pattern to the jQueryUI DatePicker TimePicker addon Javascript format (or throws * PatternNotConvertableException). * @param patternDecomposition * @param patternsA * @param patternsB * @param escapeInsideAllowed * @return * @throws PatternNotConvertableException */ @SuppressWarnings({ "ImplicitNumericConversion", "OverlyLongMethod", "OverlyComplexMethod" }) private String conversion(List<String> patternDecomposition, Map<String, String> patternsA, Map<String, String> patternsB, boolean escapeInsideAllowed) throws PatternNotConvertableException { boolean firstUsablePatternRecognized = false; boolean otherPatternRecognized = false; StringBuffer unknownPatterns = new StringBuffer(); StringBuilder result = new StringBuilder(); StringBuffer conditionalResult = new StringBuffer(); for (String patternPart : patternDecomposition) { char leadLetter = patternPart.charAt(0); if (leadLetter == '\'') { if (firstUsablePatternRecognized && !escapeInsideAllowed) { throw new PatternNotConvertableException( "Escaped parts in pattern are not allowed: " + patternPart); } //add escaped part without further analyzing it conditionalResult.append(patternPart); } else if (Character.isLetter(leadLetter)) { if (patternsA.containsKey(patternPart)) { firstUsablePatternRecognized = true; if (conditionalResult.length() > 0) { if (otherPatternRecognized) { conditionalResult = new StringBuffer(); } else { result.append(conditionalResult); conditionalResult = new StringBuffer(); } } result.append(patternsA.get(patternPart)); } else if (patternsB.containsKey(patternPart)) { otherPatternRecognized = true; conditionalResult = new StringBuffer(); if (firstUsablePatternRecognized) { break; } } else { if (unknownPatterns.length() > 0) { unknownPatterns.append(", "); } unknownPatterns.append(patternPart); } } else if (specialCharacters.contains(new Character(patternPart.charAt(0)))) { if (firstUsablePatternRecognized) { result.append(patternPart); } } else { conditionalResult.append(patternPart); } } if (unknownPatterns.length() > 0) { throw new PatternNotConvertableException( "Java pattern contains unconvertable chunks, that are not accepted by jQuery UI DatePicker: " + unknownPatterns); } return result.toString(); } /** * Adds current value of a buffer to pattern decomposition and creates new one (only if it contains some value). * @param patternDecomposition * @param buffer * @return */ private StringBuilder resetBuffer(List<String> patternDecomposition, StringBuilder buffer) { if (buffer.length() > 0) { patternDecomposition.add(buffer.toString()); buffer = new StringBuilder(); } return buffer; } /** * Contains pattern definition along with compiled version of SimpleDateFormatType to fast use. */ private static class DateFormatHolder { private final String format; private final boolean combinable; private final Map<Locale, SimpleDateFormat> formats = new HashMap<Locale, SimpleDateFormat>(); private DateFormatHolder(String format, boolean combinable) { this.format = format; this.combinable = combinable; } public SimpleDateFormat getFormat(Locale locale) { SimpleDateFormat sdf = formats.get(locale); if (sdf == null) { synchronized (formats) { sdf = new SimpleDateFormat(format, locale); sdf.setLenient(false); formats.put(locale, sdf); } } return sdf; } public String getPattern() { return format; } public boolean isCombinable() { return combinable; } } }