Saturday, September 15, 2007

DateUtil

Useful date utilities

An utility class with some fairly simple methods and some very useful methods (such as determineDateFormat()). This utility class partly wraps the CalendarUtil so that it can be used with Date and String objects.

The determineDateFormat() method supports the most commonly used date formats. You can always change or extend it to your taste, but take care that there should be no overlap in regexp patterns! Otherwise you'd like to use LinkedHashMap instead so that the regexps will be tested in the order as they've been put in the map, so that you can arrange the match preference.

package net.balusc.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;

/**
 * Useful Date utilities.
 *
 * @author BalusC
 * @see CalendarUtil
 * @link http://balusc.omnifaces.org/2007/09/dateutil.html
 */
public final class DateUtil {

    // Init ---------------------------------------------------------------------------------------

    private static final Map<String, String> DATE_FORMAT_REGEXPS = new HashMap<String, String>() {{
        put("^\\d{8}$", "yyyyMMdd");
        put("^\\d{1,2}-\\d{1,2}-\\d{4}$", "dd-MM-yyyy");
        put("^\\d{4}-\\d{1,2}-\\d{1,2}$", "yyyy-MM-dd");
        put("^\\d{1,2}/\\d{1,2}/\\d{4}$", "MM/dd/yyyy");
        put("^\\d{4}/\\d{1,2}/\\d{1,2}$", "yyyy/MM/dd");
        put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}$", "dd MMM yyyy");
        put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}$", "dd MMMM yyyy");
        put("^\\d{12}$", "yyyyMMddHHmm");
        put("^\\d{8}\\s\\d{4}$", "yyyyMMdd HHmm");
        put("^\\d{1,2}-\\d{1,2}-\\d{4}\\s\\d{1,2}:\\d{2}$", "dd-MM-yyyy HH:mm");
        put("^\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{2}$", "yyyy-MM-dd HH:mm");
        put("^\\d{1,2}/\\d{1,2}/\\d{4}\\s\\d{1,2}:\\d{2}$", "MM/dd/yyyy HH:mm");
        put("^\\d{4}/\\d{1,2}/\\d{1,2}\\s\\d{1,2}:\\d{2}$", "yyyy/MM/dd HH:mm");
        put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}\\s\\d{1,2}:\\d{2}$", "dd MMM yyyy HH:mm");
        put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}\\s\\d{1,2}:\\d{2}$", "dd MMMM yyyy HH:mm");
        put("^\\d{14}$", "yyyyMMddHHmmss");
        put("^\\d{8}\\s\\d{6}$", "yyyyMMdd HHmmss");
        put("^\\d{1,2}-\\d{1,2}-\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd-MM-yyyy HH:mm:ss");
        put("^\\d{4}-\\d{1,2}-\\d{1,2}\\s\\d{1,2}:\\d{2}:\\d{2}$", "yyyy-MM-dd HH:mm:ss");
        put("^\\d{1,2}/\\d{1,2}/\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "MM/dd/yyyy HH:mm:ss");
        put("^\\d{4}/\\d{1,2}/\\d{1,2}\\s\\d{1,2}:\\d{2}:\\d{2}$", "yyyy/MM/dd HH:mm:ss");
        put("^\\d{1,2}\\s[a-z]{3}\\s\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd MMM yyyy HH:mm:ss");
        put("^\\d{1,2}\\s[a-z]{4,}\\s\\d{4}\\s\\d{1,2}:\\d{2}:\\d{2}$", "dd MMMM yyyy HH:mm:ss");
    }};

    private DateUtil() {
        // Utility class, hide the constructor.
    }

    // Converters ---------------------------------------------------------------------------------

    /**
     * Convert the given date to a Calendar object. The TimeZone will be derived from the local
     * operating system's timezone.
     * @param date The date to be converted to Calendar.
     * @return The Calendar object set to the given date and using the local timezone.
     */
    public static Calendar toCalendar(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.setTime(date);
        return calendar;
    }

    /**
     * Convert the given date to a Calendar object with the given timezone.
     * @param date The date to be converted to Calendar.
     * @param timeZone The timezone to be set in the Calendar.
     * @return The Calendar object set to the given date and timezone.
     */
    public static Calendar toCalendar(Date date, TimeZone timeZone) {
        Calendar calendar = toCalendar(date);
        calendar.setTimeZone(timeZone);
        return calendar;
    }

    /**
     * Parse the given date string to date object and return a date instance based on the given
     * date string. This makes use of the {@link DateUtil#determineDateFormat(String)} to determine
     * the SimpleDateFormat pattern to be used for parsing.
     * @param dateString The date string to be parsed to date object.
     * @return The parsed date object.
     * @throws ParseException If the date format pattern of the given date string is unknown, or if
     * the given date string or its actual date is invalid based on the date format pattern.
     */
    public static Date parse(String dateString) throws ParseException {
        String dateFormat = determineDateFormat(dateString);
        if (dateFormat == null) {
            throw new ParseException("Unknown date format.", 0);
        }
        return parse(dateString, dateFormat);
    }

    /**
     * Validate the actual date of the given date string based on the given date format pattern and
     * return a date instance based on the given date string.
     * @param dateString The date string.
     * @param dateFormat The date format pattern which should respect the SimpleDateFormat rules.
     * @return The parsed date object.
     * @throws ParseException If the given date string or its actual date is invalid based on the
     * given date format pattern.
     * @see SimpleDateFormat
     */
    public static Date parse(String dateString, String dateFormat) throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat);
        simpleDateFormat.setLenient(false); // Don't automatically convert invalid date.
        return simpleDateFormat.parse(dateString);
    }

    // Validators ---------------------------------------------------------------------------------

    /**
     * Checks whether the actual date of the given date string is valid. This makes use of the
     * {@link DateUtil#determineDateFormat(String)} to determine the SimpleDateFormat pattern to be
     * used for parsing.
     * @param dateString The date string.
     * @return True if the actual date of the given date string is valid.
     */
    public static boolean isValidDate(String dateString) {
        try {
            parse(dateString);
            return true;
        } catch (ParseException e) {
            return false;
        }
    }

    /**
     * Checks whether the actual date of the given date string is valid based on the given date
     * format pattern.
     * @param dateString The date string.
     * @param dateFormat The date format pattern which should respect the SimpleDateFormat rules.
     * @return True if the actual date of the given date string is valid based on the given date
     * format pattern.
     * @see SimpleDateFormat
     */
    public static boolean isValidDate(String dateString, String dateFormat) {
        try {
            parse(dateString, dateFormat);
            return true;
        } catch (ParseException e) {
            return false;
        }
    }

    // Checkers -----------------------------------------------------------------------------------

    /**
     * Determine SimpleDateFormat pattern matching with the given date string. Returns null if
     * format is unknown. You can simply extend DateUtil with more formats if needed.
     * @param dateString The date string to determine the SimpleDateFormat pattern for.
     * @return The matching SimpleDateFormat pattern, or null if format is unknown.
     * @see SimpleDateFormat
     */
    public static String determineDateFormat(String dateString) {
        for (String regexp : DATE_FORMAT_REGEXPS.keySet()) {
            if (dateString.toLowerCase().matches(regexp)) {
                return DATE_FORMAT_REGEXPS.get(regexp);
            }
        }
        return null; // Unknown format.
    }

    // Changers -----------------------------------------------------------------------------------

    /**
     * Add the given amount of years to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addYears(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of years to.
     * @param years The amount of years to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addYears(Date date, int years) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addYears(calendar, years);
        return calendar.getTime();
    }

    /**
     * Add the given amount of months to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addMonths(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of months to.
     * @param months The amount of months to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addMonths(Date date, int months) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addMonths(calendar, months);
        return calendar.getTime();
    }

    /**
     * Add the given amount of days to the given date. It actually converts the date to Calendar and
     * calls {@link CalendarUtil#addDays(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of days to.
     * @param days The amount of days to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addDays(Date date, int days) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addDays(calendar, days);
        return calendar.getTime();
    }

    /**
     * Add the given amount of hours to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addHours(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of hours to.
     * @param hours The amount of hours to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addHours(Date date, int hours) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addHours(calendar, hours);
        return calendar.getTime();
    }

    /**
     * Add the given amount of minutes to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addMinutes(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of minutes to.
     * @param minutes The amount of minutes to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addMinutes(Date date, int minutes) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addMinutes(calendar, minutes);
        return calendar.getTime();
    }

    /**
     * Add the given amount of seconds to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addSeconds(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of seconds to.
     * @param seconds The amount of seconds to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addSeconds(Date date, int seconds) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addSeconds(calendar, seconds);
        return calendar.getTime();
    }

    /**
     * Add the given amount of millis to the given date. It actually converts the date to Calendar
     * and calls {@link CalendarUtil#addMillis(Calendar, int)} and then converts back to date.
     * @param date The date to add the given amount of millis to.
     * @param millis The amount of millis to be added to the given date. Negative values are also
     * allowed, it will just go back in time.
     */
    public static Date addMillis(Date date, int millis) {
        Calendar calendar = toCalendar(date);
        CalendarUtil.addMillis(calendar, millis);
        return calendar.getTime();
    }

    // Comparators --------------------------------------------------------------------------------

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year. It actually
     * converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameYear(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year.
     * @see CalendarUtil#sameYear(Calendar, Calendar)
     */
    public static boolean sameYear(Date one, Date two) {
        return CalendarUtil.sameYear(toCalendar(one), toCalendar(two));
    }

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year and month. It
     * actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameMonth(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year and month.
     * @see CalendarUtil#sameMonth(Calendar, Calendar)
     */
    public static boolean sameMonth(Date one, Date two) {
        return CalendarUtil.sameMonth(toCalendar(one), toCalendar(two));
    }

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year, month and day. It
     * actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameDay(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year, month and day.
     * @see CalendarUtil#sameDay(Calendar, Calendar)
     */
    public static boolean sameDay(Date one, Date two) {
        return CalendarUtil.sameDay(toCalendar(one), toCalendar(two));
    }

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year, month, day and
     * hour. It actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameHour(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year, month, day and hour.
     * @see CalendarUtil#sameHour(Calendar, Calendar)
     */
    public static boolean sameHour(Date one, Date two) {
        return CalendarUtil.sameHour(toCalendar(one), toCalendar(two));
    }

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year, month, day, hour
     * and minute. It actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameMinute(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year, month, day, hour and minute.
     * @see CalendarUtil#sameMinute(Calendar, Calendar)
     */
    public static boolean sameMinute(Date one, Date two) {
        return CalendarUtil.sameMinute(toCalendar(one), toCalendar(two));
    }

    /**
     * Returns <tt>true</tt> if the two given dates are dated on the same year, month, day, hour,
     * minute and second. It actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#sameSecond(Calendar, Calendar)}.
     * @param one The one date.
     * @param two The other date.
     * @return True if the two given dates are dated on the same year, month, day, hour, minute and
     * second.
     * @see CalendarUtil#sameSecond(Calendar, Calendar)
     */
    public static boolean sameSecond(Date one, Date two) {
        return CalendarUtil.sameSecond(toCalendar(one), toCalendar(two));
    }

    // Calculators --------------------------------------------------------------------------------

    /**
     * Retrieve the amount of elapsed years between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedYears(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed years between the two given dates
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedYears(Calendar, Calendar)
     */
    public static int elapsedYears(Date before, Date after) {
        return CalendarUtil.elapsedYears(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed months between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedMonths(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed months between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedMonths(Calendar, Calendar)
     */
    public static int elapsedMonths(Date before, Date after) {
        return CalendarUtil.elapsedMonths(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed days between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedDays(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed days between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedDays(Calendar, Calendar)
     */
    public static int elapsedDays(Date before, Date after) {
        return CalendarUtil.elapsedDays(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed hours between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedHours(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed hours between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedHours(Calendar, Calendar)
     */
    public static int elapsedHours(Date before, Date after) {
        return CalendarUtil.elapsedHours(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed minutes between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedMinutes(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed minutes between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedMinutes(Calendar, Calendar)
     */
    public static int elapsedMinutes(Date before, Date after) {
        return CalendarUtil.elapsedMinutes(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed seconds between the two given dates. It actually converts the
     * both dates to Calendar and calls {@link CalendarUtil#elapsedSeconds(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed seconds between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedSeconds(Calendar, Calendar)
     */
    public static int elapsedSeconds(Date before, Date after) {
        return CalendarUtil.elapsedSeconds(toCalendar(before), toCalendar(after));
    }

    /**
     * Retrieve the amount of elapsed milliseconds between the two given dates. It actually converts
     * the both dates to Calendar and calls {@link CalendarUtil#elapsedMillis(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The amount of elapsed milliseconds between the two given dates.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedMillis(Calendar, Calendar)
     */
    public static long elapsedMillis(Date before, Date after) {
        return CalendarUtil.elapsedMillis(toCalendar(before), toCalendar(after));
    }

    /**
     * Calculate the total of elapsed time from years up to seconds between the two given dates. It
     * Returns an int array with the elapsed years, months, days, hours, minutes and seconds
     * respectively. It actually converts the both dates to Calendar and calls
     * {@link CalendarUtil#elapsedTime(Calendar, Calendar)}.
     * @param before The first date with expected date before the second date.
     * @param after The second date with expected date after the first date.
     * @return The elapsed time between the two given dates in years, months, days, hours, minutes
     * and seconds.
     * @throws IllegalArgumentException If the first date is dated after the second date.
     * @see CalendarUtil#elapsedTime(Calendar, Calendar)
     */
    public static int[] elapsedTime(Date before, Date after) {
        return CalendarUtil.elapsedTime(toCalendar(before), toCalendar(after));
    }

}

Copyright - WTFPL

(C) September 2007, BalusC

CalendarUtil

Useful calendar utilities

An utility class with some fairly simple methods and some very useful methods. The elapsedXxxx methods are absolutely useful. If you want to use it with java.util.Date or even java.lang.String objects, then you may find the DateUtil more useful.

package net.balusc.util;

import java.util.Calendar;

/**
 * Useful Calendar utilities.
 *
 * @author BalusC
 * @link http://balusc.omnifaces.org/2007/09/calendarutil.html
 */
public final class CalendarUtil {

    // Init ---------------------------------------------------------------------------------------

    private CalendarUtil() {
        // Utility class, hide the constructor.
    }

    // Validators ---------------------------------------------------------------------------------

    /**
     * Checks whether the given day, month and year combination is a valid date or not.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @return True if the given day, month and year combination is a valid date.
     */
    public static boolean isValidDate(int year, int month, int day) {
        return isValidDate(year, month, day, 0, 0, 0);
    }

    /**
     * Checks whether the given hour, minute and second combination is a valid time or not.
     * @param hour The hour part of the time.
     * @param minute The minute part of the time.
     * @param second The second part of the time.
     * @return True if the given hour, minute and second combination is a valid time.
     */
    public static boolean isValidTime(int hour, int minute, int second) {
        return isValidDate(1, 1, 1, hour, minute, second);
    }

    /**
     * Checks whether the given day, month, year, hour, minute and second combination is a valid
     * date or not.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @param hour The hour part of the date.
     * @param minute The minute part of the date.
     * @param second The second part of the date.
     * @return True if the given day, month, year, hour, minute and second combination is a valid
     * date.
     */
    public static boolean isValidDate(
        int year, int month, int day, int hour, int minute, int second)
    {
        try {
            getValidCalendar(year, month, day, hour, minute, second);
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * Validate the actual date of the given date elements and returns a calendar instance based on
     * the given date elements. The time is forced to 00:00:00.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @return A Calendar instance prefilled with the given date elements.
     * @throws IllegalArgumentException If the given date elements does not represent a valid date.
     */
    public static Calendar getValidCalendar(int year, int month, int day) {
        return getValidCalendar(year, month, day, 0, 0, 0);
    }

    /**
     * Validate the actual date of the given date elements and returns a calendar instance based on
     * the given date elements.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @param hour The hour part of the date.
     * @param minute The minute part of the date.
     * @param second The second part of the date.
     * @return A Calendar instance prefilled with the given date elements.
     * @throws IllegalArgumentException If the given date elements does not represent a valid date.
     */
    public static Calendar getValidCalendar(
        int year, int month, int day, int hour, int minute, int second)
    {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.setLenient(false); // Don't automatically convert invalid date.
        calendar.set(year, month - 1, day, hour, minute, second);
        calendar.getTimeInMillis(); // Lazy update, throws IllegalArgumentException if invalid date.
        return calendar;
    }

    // Changers -----------------------------------------------------------------------------------

    /**
     * Add the given amount of years to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of years to.
     * @param years The amount of years to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addYears(Calendar calendar, int years) {
        calendar.add(Calendar.YEAR, years);
    }

    /**
     * Add the given amount of months to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of months to.
     * @param months The amount of months to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMonths(Calendar calendar, int months) {
        calendar.add(Calendar.MONTH, months);
    }

    /**
     * Add the given amount of days to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of days to.
     * @param days The amount of days to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addDays(Calendar calendar, int days) {
        calendar.add(Calendar.DATE, days);
    }

    /**
     * Add the given amount of hours to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of hours to.
     * @param hours The amount of hours to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addHours(Calendar calendar, int hours) {
        calendar.add(Calendar.HOUR, hours);
    }

    /**
     * Add the given amount of minutes to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of minutes to.
     * @param minutes The amount of minutes to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMinutes(Calendar calendar, int minutes) {
        calendar.add(Calendar.MINUTE, minutes);
    }

    /**
     * Add the given amount of seconds to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of seconds to.
     * @param seconds The amount of seconds to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addSeconds(Calendar calendar, int seconds) {
        calendar.add(Calendar.SECOND, seconds);
    }

    /**
     * Add the given amount of millis to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of millis to.
     * @param millis The amount of millis to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMillis(Calendar calendar, int millis) {
        calendar.add(Calendar.MILLISECOND, millis);
    }

    // Comparators --------------------------------------------------------------------------------

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year.
     */
    public static boolean sameYear(Calendar one, Calendar two) {
        return one.get(Calendar.YEAR) == two.get(Calendar.YEAR);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year and month.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year and month.
     */
    public static boolean sameMonth(Calendar one, Calendar two) {
        return one.get(Calendar.MONTH) == two.get(Calendar.MONTH) && sameYear(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month and day.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month and day.
     */
    public static boolean sameDay(Calendar one, Calendar two) {
        return one.get(Calendar.DATE) == two.get(Calendar.DATE) && sameMonth(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day and
     * hour.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day and hour.
     */
    public static boolean sameHour(Calendar one, Calendar two) {
        return one.get(Calendar.HOUR_OF_DAY) == two.get(Calendar.HOUR_OF_DAY) && sameDay(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day,
     * hour and minute.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day, hour and
     * minute.
     */
    public static boolean sameMinute(Calendar one, Calendar two) {
        return one.get(Calendar.MINUTE) == two.get(Calendar.MINUTE) && sameHour(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day,
     * hour, minute and second.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day, hour, minute
     * and second.
     */
    public static boolean sameSecond(Calendar one, Calendar two) {
        return one.get(Calendar.SECOND) == two.get(Calendar.SECOND) && sameMinute(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same time. The difference
     * from <tt>one.equals(two)</tt> is that this method does not respect the time zone.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same time.
     */
    public static boolean sameTime(Calendar one, Calendar two) {
        return one.getTimeInMillis() == two.getTimeInMillis();
    }

    // Calculators --------------------------------------------------------------------------------

    /**
     * Retrieve the amount of elapsed years between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed years between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedYears(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.YEAR);
    }

    /**
     * Retrieve the amount of elapsed months between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed months between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedMonths(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.MONTH);
    }

    /**
     * Retrieve the amount of elapsed days between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed days between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedDays(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.DATE);
    }

    /**
     * Retrieve the amount of elapsed hours between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed hours between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedHours(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 3600000); // 1h = 60m = 3600s = 3600000ms
    }

    /**
     * Retrieve the amount of elapsed minutes between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed minutes between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedMinutes(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 60000); // 1m = 60s = 60000ms
    }

    /**
     * Retrieve the amount of elapsed seconds between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed seconds between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedSeconds(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 1000); // 1sec = 1000ms.
    }

    /**
     * Retrieve the amount of elapsed milliseconds between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed milliseconds between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static long elapsedMillis(Calendar before, Calendar after) {
        return elapsedMillis(before, after, 1); // 1ms is apparently 1ms.
    }

    /**
     * Calculate the total of elapsed time from years up to seconds between the two given calendars.
     * It returns an int array with the elapsed years, months, days, hours, minutes and seconds
     * respectively.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The elapsed time between the two given calendars in years, months, days, hours,
     * minutes and seconds.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int[] elapsedTime(Calendar before, Calendar after) {
        int[] elapsedTime = new int[6];
        Calendar clone = (Calendar) before.clone(); // Otherwise changes are been reflected.

        elapsedTime[0] = elapsedYears(clone, after);
        addYears(clone, elapsedTime[0]);

        elapsedTime[1] = elapsedMonths(clone, after);
        addMonths(clone, elapsedTime[1]);

        elapsedTime[2] = elapsedDays(clone, after);
        addDays(clone, elapsedTime[2]);

        elapsedTime[3] = elapsedHours(clone, after);
        addHours(clone, elapsedTime[3]);

        elapsedTime[4] = elapsedMinutes(clone, after);
        addMinutes(clone, elapsedTime[4]);

        elapsedTime[5] = elapsedSeconds(clone, after);

        return elapsedTime;
    }

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Retrieve the amount of elapsed time between the two given calendars based on the given
     * calendar field as definied in the Calendar constants, e.g. <tt>Calendar.MONTH</tt>.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @param field The calendar field as definied in the Calendar constants.
     * @return The amount of elapsed time between the two given calendars based on the given
     * calendar field.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    private static int elapsed(Calendar before, Calendar after, int field) {
        checkBeforeAfter(before, after);
        Calendar clone = (Calendar) before.clone(); // Otherwise changes are been reflected.
        int elapsed = -1;
        while (!clone.after(after)) {
            clone.add(field, 1);
            elapsed++;
        }
        return elapsed;
    }

    /**
     * Retrieve the amount of elapsed milliseconds between the two given calendars and directly
     * divide the outcome by the given factor. E.g.: if the division factor is 1, then you will get
     * the elapsed milliseconds unchanged; if the division factor is 1000, then the elapsed
     * milliseconds will be divided by 1000, resulting in the amount of elapsed seconds.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @param factor The division factor which to divide the milliseconds with, expected to be at
     * least 1.
     * @return The amount of elapsed milliseconds between the two given calendars, divided by the
     * given factor.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar or
     * if the division factor is less than 1.
     */
    private static long elapsedMillis(Calendar before, Calendar after, int factor) {
        checkBeforeAfter(before, after);
        if (factor < 1) {
            throw new IllegalArgumentException(
                "Division factor '" + factor + "' should not be less than 1.");
        }
        return (after.getTimeInMillis() - before.getTimeInMillis()) / factor;
    }

    /**
     * Check if the first calendar is actually dated before the second calendar.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    private static void checkBeforeAfter(Calendar before, Calendar after) {
        if (before.after(after)) {
            throw new IllegalArgumentException(
                "The first calendar should be dated before the second calendar.");
        }
    }

}

Copyright - WTFPL

(C) September 2007, BalusC

Monday, September 10, 2007

FileUtil

Useful filesystem level utilities

I'd like to share this utility class with lot of useful filesystem level utility methods. It minimizes the effort of the developer to write code for java.io stuff. Just call the utility method in a try-catch-finally block and handle the eventual IOException to your taste.

/*
 * net/balusc/util/FileUtil.java
 * 
 * Copyright (C) 2007 BalusC
 * 
 * This program is free software: you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation, either version 3
 * of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with this library.
 * If not, see <http://www.gnu.org/licenses/>.
 */

package net.balusc.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.CharArrayReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

/**
 * Useful file system level utilities.
 * 
 * @author BalusC
 * @link http://balusc.blogspot.com/2007/09/FileUtil.html
 */
public final class FileUtil {

    // Init ---------------------------------------------------------------------------------------

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    // Constructors -------------------------------------------------------------------------------

    private FileUtil() {
        // Utility class, hide constructor.
    }

    // Writers ------------------------------------------------------------------------------------

    /**
     * Write byte array to file. If file already exists, it will be overwritten.
     * @param file The file where the given byte array have to be written to.
     * @param bytes The byte array which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, byte[] bytes) throws IOException {
        write(file, new ByteArrayInputStream(bytes), false);
    }

    /**
     * Write byte array to file with option to append to file or not. If not, then any existing
     * file will be overwritten.
     * @param file The file where the given byte array have to be written to.
     * @param bytes The byte array which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, byte[] bytes, boolean append) throws IOException {
        write(file, new ByteArrayInputStream(bytes), append);
    }

    /**
     * Write byte inputstream to file. If file already exists, it will be overwritten.It's highly
     * recommended to feed the inputstream as BufferedInputStream or ByteArrayInputStream as those
     * are been automatically buffered.
     * @param file The file where the given byte inputstream have to be written to.
     * @param input The byte inputstream which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, InputStream input) throws IOException {
        write(file, input, false);
    }

    /**
     * Write byte inputstream to file with option to append to file or not. If not, then any
     * existing file will be overwritten. It's highly recommended to feed the inputstream as
     * BufferedInputStream or ByteArrayInputStream as those are been automatically buffered.
     * @param file The file where the given byte inputstream have to be written to.
     * @param input The byte inputstream which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, InputStream input, boolean append) throws IOException {
        mkdirs(file);
        BufferedOutputStream output = null;

        try {
            output = new BufferedOutputStream(new FileOutputStream(file, append));
            int data = -1;
            while ((data = input.read()) != -1) {
                output.write(data);
            }
        } finally {
            close(input, file);
            close(output, file);
        }
    }

    /**
     * Write character array to file. If file already exists, it will be overwritten.
     * @param file The file where the given character array have to be written to.
     * @param chars The character array which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, char[] chars) throws IOException {
        write(file, new CharArrayReader(chars), false);
    }

    /**
     * Write character array to file with option to append to file or not. If not, then any
     * existing file will be overwritten.
     * @param file The file where the given character array have to be written to.
     * @param chars The character array which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, char[] chars, boolean append) throws IOException {
        write(file, new CharArrayReader(chars), append);
    }

    /**
     * Write string value to file. If file already exists, it will be overwritten.
     * @param file The file where the given string value have to be written to.
     * @param string The string value which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, String string) throws IOException {
        write(file, new CharArrayReader(string.toCharArray()), false);
    }

    /**
     * Write string value to file with option to append to file or not. If not, then any existing
     * file will be overwritten.
     * @param file The file where the given string value have to be written to.
     * @param string The string value which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, String string, boolean append) throws IOException {
        write(file, new CharArrayReader(string.toCharArray()), append);
    }

    /**
     * Write character reader to file. If file already exists, it will be overwritten. It's highly
     * recommended to feed the reader as BufferedReader or CharArrayReader as those are been
     * automatically buffered.
     * @param file The file where the given character reader have to be written to.
     * @param reader The character reader which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, Reader reader) throws IOException {
        write(file, reader, false);
    }

    /**
     * Write character reader to file with option to append to file or not. If not, then any
     * existing file will be overwritten. It's highly recommended to feed the reader as
     * BufferedReader or CharArrayReader as those are been automatically buffered.
     * @param file The file where the given character reader have to be written to.
     * @param reader The character reader which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, Reader reader, boolean append) throws IOException {
        mkdirs(file);
        BufferedWriter writer = null;

        try {
            writer = new BufferedWriter(new FileWriter(file, append));
            int data = -1;
            while ((data = reader.read()) != -1) {
                writer.write(data);
            }
        } finally {
            close(reader, file);
            close(writer, file);
        }
    }

    /**
     * Write list of String records to file. If file already exists, it will be overwritten.
     * @param file The file where the given character reader have to be written to.
     * @param records The list of String records which have to be written to the given file.
     * @throws IOException If writing file fails.
     */
    public static void write(File file, List<String> records) throws IOException {
        write(file, records, false);
    }

    /**
     * Write list of String records to file with option to append to file or not. If not, then any
     * existing file will be overwritten.
     * @param file The file where the given character reader have to be written to.
     * @param records The list of String records which have to be written to the given file.
     * @param append Append to file?
     * @throws IOException If writing file fails.
     */
    public static void write(File file, List<String> records, boolean append) throws IOException {
        mkdirs(file);
        BufferedWriter writer = null;

        try {
            writer = new BufferedWriter(new FileWriter(file, append));
            for (String record : records) {
                writer.write(record);
                writer.write(LINE_SEPARATOR);
            }
        } finally {
            close(writer, file);
        }
    }

    // Readers ------------------------------------------------------------------------------------

    /**
     * Read byte array from file. Take care with big files, this would be memory hogging, rather
     * use readStream() instead.
     * @param file The file to read the byte array from.
     * @return The byte array with the file contents.
     * @throws IOException If reading file fails.
     */
    public static byte[] readBytes(File file) throws IOException {
        BufferedInputStream stream = (BufferedInputStream) readStream(file);
        byte[] bytes = new byte[stream.available()];
        stream.read(bytes);
        return bytes;
    }

    /**
     * Read byte stream from file.
     * @param file The file to read the byte stream from.
     * @return The byte stream with the file contents (actually: BufferedInputStream).
     * @throws IOException If reading file fails.
     */
    public static InputStream readStream(File file) throws IOException {
        return new BufferedInputStream(new FileInputStream(file));
    }

    /**
     * Read character array from file. Take care with big files, this would be memory hogging,
     * rather use readReader() instead.
     * @param file The file to read the character array from.
     * @return The character array with the file contents.
     * @throws IOException If reading file fails.
     */
    public static char[] readChars(File file) throws IOException {
        BufferedReader reader = (BufferedReader) readReader(file);
        char[] chars = new char[(int) file.length()];
        reader.read(chars);
        return chars;
    }

    /**
     * Read string value from file. Take care with big files, this would be memory hogging, rather
     * use readReader() instead.
     * @param file The file to read the string value from.
     * @return The string value with the file contents.
     * @throws IOException If reading file fails.
     */
    public static String readString(File file) throws IOException {
        return new String(readChars(file));
    }

    /**
     * Read character reader from file.
     * @param file The file to read the character reader from.
     * @return The character reader with the file contents (actually: BufferedReader).
     * @throws IOException If reading file fails.
     */
    public static Reader readReader(File file) throws IOException {
        return new BufferedReader(new FileReader(file));
    }

    /**
     * Read list of String records from file.
     * @param file The file to read the character writer from.
     * @return A list of String records which represents lines of the file contents.
     * @throws IOException If reading file fails.
     */
    public static List<String> readRecords(File file) throws IOException {
        BufferedReader reader = (BufferedReader) readReader(file);
        List<String> records = new ArrayList<String>();
        String record = null;

        try {
            while ((record = reader.readLine()) != null) {
                records.add(record);
            }
        } finally {
            close(reader, file);
        }

        return records;
    }

    // Copiers ------------------------------------------------------------------------------------

    /**
     * Copy file. Any existing file at the destination will be overwritten.
     * @param source The file to read the contents from.
     * @param destination The file to write the contents to.
     * @throws IOException If copying file fails.
     */
    public static void copy(File source, File destination) throws IOException {
        copy(source, destination, true);
    }

    /**
     * Copy file with the option to overwrite any existing file at the destination.
     * @param source The file to read the contents from.
     * @param destination The file to write the contents to.
     * @param overwrite Set whether to overwrite any existing file at the destination.
     * @throws IOException If the destination file already exists while <tt>overwrite</tt> is set
     * to false, or if copying file fails.
     */
    public static void copy(File source, File destination, boolean overwrite) throws IOException {
        if (destination.exists() && !overwrite) {
            throw new IOException(
                "Copying file " + source.getPath() + " to " + destination.getPath() + " failed."
                    + " The destination file already exists.");
        }

        mkdirs(destination);
        BufferedInputStream input = null;
        BufferedOutputStream output = null;

        try {
            input = new BufferedInputStream(new FileInputStream(source));
            output = new BufferedOutputStream(new FileOutputStream(destination));
            int data = -1;
            while ((data = input.read()) != -1) {
                output.write(data);
            }
        } finally {
            close(input, source);
            close(output, destination);
        }
    }

    // Movers -------------------------------------------------------------------------------------

    /**
     * Move (rename) file. Any existing file at the destination will be overwritten.
     * @param source The file to be moved.
     * @param destination The new destination of the file.
     * @throws IOException If moving file fails.
     */
    public static void move(File source, File destination) throws IOException {
        move(source, destination, true);
    }

    /**
     * Move (rename) file with the option to overwrite any existing file at the destination.
     * @param source The file to be moved.
     * @param destination The new destination of the file.
     * @param overwrite Set whether to overwrite any existing file at the destination.
     * @throws IOException If the destination file already exists while <tt>overwrite</tt> is set
     * to false, or if moving file fails.
     */
    public static void move(File source, File destination, boolean overwrite) throws IOException {
        if (destination.exists()) {
            if (overwrite) {
                destination.delete();
            } else {
                throw new IOException(
                    "Moving file " + source.getPath() + " to " + destination.getPath() + " failed."
                        + " The destination file already exists.");
            }
        }

        mkdirs(destination);

        if (!source.renameTo(destination)) {
            throw new IOException(
                "Moving file " + source.getPath() + " to " + destination.getPath() + " failed.");
        }
    }

    // Filenames ----------------------------------------------------------------------------------

    /**
     * Trim the eventual file path from the given file name. Anything before the last occurred "/"
     * and "\" will be trimmed, including the slash.
     * @param fileName The file name to trim the file path from.
     * @return The file name with the file path trimmed.
     */
    public static String trimFilePath(String fileName) {
        return fileName
            .substring(fileName.lastIndexOf("/") + 1)
            .substring(fileName.lastIndexOf("\\") + 1);
    }

    /**
     * Generate unique file based on the given path and name. If the file exists, then it will
     * add "[i]" to the file name as long as the file exists. The value of i can be between
     * 0 and 2147483647 (the value of Integer.MAX_VALUE).
     * @param filePath The path of the unique file.
     * @param fileName The name of the unique file.
     * @return The unique file.
     * @throws IOException If unique file cannot be generated, this can be caused if all file
     * names are already in use. You may consider another filename instead.
     */
    public static File uniqueFile(File filePath, String fileName) throws IOException {
        File file = new File(filePath, fileName);
        
        if (file.exists()) {

            // Split filename and add braces, e.g. "name.ext" --> "name[", "].ext".
            String prefix;
            String suffix;
            int dotIndex = fileName.lastIndexOf(".");

            if (dotIndex > -1) {
                prefix = fileName.substring(0, dotIndex) + "[";
                suffix = "]" + fileName.substring(dotIndex);
            } else {
                prefix = fileName + "[";
                suffix = "]";
            }

            int count = 0;

            // Add counter to filename as long as file exists.
            while (file.exists()) {
                if (count < 0) { // int++ restarts at -2147483648 after 2147483647.
                    throw new IOException("No unique filename available for " + fileName 
                        + " in path " + filePath.getPath() + ".");
                }

                // Glue counter between prefix and suffix, e.g. "name[" + count + "].ext".
                file = new File(filePath, prefix + (count++) + suffix);
            }
        }

        return file;
    }

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Check and create missing parent directories for the given file.
     * @param file The file to check and create the missing parent directories for.
     * @throws IOException If the given file is actually not a file or if creating parent 
     * directories fails.
     */
    private static void mkdirs(File file) throws IOException {
        if (file.exists() && !file.isFile()) {
            throw new IOException("File " + file.getPath() + " is actually not a file.");
        }
        File parentFile = file.getParentFile();
        if (!parentFile.exists() && !parentFile.mkdirs()) {
            throw new IOException("Creating directories " + parentFile.getPath() + " failed.");
        }
    }

    /**
     * Close the given I/O resource of the given file.
     * @param resource The I/O resource to be closed.
     * @param file The I/O resource's subject.
     */
    private static void close(Closeable resource, File file) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException e) {
                String message = "Closing file " + file.getPath() + " failed.";
                // Do your thing with the exception and the message. Print it, log it or mail it.
                System.err.println(message);
                e.printStackTrace();
            }
        }
    }
    
}

Copyright - GNU Lesser General Public License

(C) September 2007, BalusC

Friday, September 7, 2007

Objects in h:selectOneMenu

WARNING - OUTDATED CONTENT!

This article is targeted on JSF 1.2. For JSF 2.0, using objects in UISelectOne and UISelectMany components can be approached much more elegantly with help of OmniFaces SelectItemsConverter without the need to write a custom converter.

Introduction

A HTML response is in Object Oriented perspective nothing less or more than one big String value (technically: a long character array). Non-String-typed Java Objects are forced to their String representation using Object#toString() when they are about to be written to the HTML response. That's why using custom Object types in a UISelectOne or UISelectMany component, for example h:selectOneMenu, can drive unaware JSF developers nuts. If you gently have attached a <h:message /> to the component for error messages, or added the <h:messages /> tag to the page for debugging purposes, then you will likely get the following error message:

Conversion Error setting value 'mypackage.Foo@1234567' for 'null Converter'.

That roughly means that JSF cannot convert the given String value "mypackage.Foo@1234567" (which is just the String representation of the Object, obtained by Object#toString()) to the Object type which is expected by the valuebinding of the component, e.g. mypackage.Foo. The message also indicates that JSF couldn't find a suitable converter for it, pointing to the null ID in the 'null Converter' message part. Generally this will only occur if those objects are not of a String type and JSF does not have a built-in converter for it as well. That's why, next to plain String values, for example the types of the Number superclass just works in the h:selectOneMenu. At least in the newer builds of JSF, the 1.1_02 and 1.2_02 or later, due to some coerce bugs with Number types in SelectItem in the older builds.

There are two general solutions for this conversion problem: 1) implement javax.faces.convert.Converter and write logic which converts between String (or Number) and the desired Object. 2) maintain a backing map with an unique String (or Number) representation as key and the appropriate Object as value. Both approaches will be descibed here in detail.

Back to top

Using a Converter

Using a Converter is fairly simple. You just need to write some logic how to Convert between String and the desired Object type. Instead of String you can also use any Number type, for example Long, because JSF already has built-in converters for it.

Let's start with a relevant part of the JSF page:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:selectItems value="#{myBean.selectItems}" />
        <f:converter converterId="fooConverter" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:messages />
</h:form>

Note the f:converter facet. The value of its converterId attribute must match with one of the converters as definied in the faces-config.xml. The faces-config.xml example will be shown later in this paragraph.

And here is the appropriate backing bean MyBean:

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Init ---------------------------------------------------------------------------------------

    private static FooDAO fooDAO = new FooDAO();
    private List<SelectItem> selectItems;
    private Foo selectedItem;

    {
        fillSelectItems();
    }

    // Actions ------------------------------------------------------------------------------------

    public void action() {
        System.out.println("Selected Foo item: " + selectedItem);
    }

    // Getters ------------------------------------------------------------------------------------

    public List<SelectItem> getSelectItems() {
        return selectItems;
    }

    public Foo getSelectedItem() {
        return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setSelectedItem(Foo selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        for (Foo foo : fooDAO.list()) {
            selectItems.add(new SelectItem(foo, foo.getValue()));
        }
    }

}

This is how the Foo object type look like. It is actually a random Data Transfer Object (DTO). Please note the Object#equals() implementation. This is very important for JSF. After conversion, it will compare the selected item against the items in the list. As the Object#equals() also require Object#hashCode(), this is implemented as well.

package mypackage;

public class Foo {

    // Init ---------------------------------------------------------------------------------------

    private String key; // You can also use any Number type, e.g. Long.
    private String value;

    // Constructors -------------------------------------------------------------------------------

    public Foo() {
        // Default constructor, keep alive.
    }

    public Foo(String key, String value) {
        this.key = key;
        this.value = value;
    }

    // Getters ------------------------------------------------------------------------------------

    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setKey(String key) {
        this.key = key;
    }

    public void setValue(String value) {
        this.value = value;
    }

    // Helpers ------------------------------------------------------------------------------------

    // This must return true for another Foo object with same key/id.
    public boolean equals(Object other) {
        return other instanceof Foo && (key != null) ? key.equals(((Foo) other).key) : (other == this);
    }

    // This must return the same hashcode for every Foo object with the same key.
    public int hashCode() {
        return key != null ? this.getClass().hashCode() + key.hashCode() : super.hashCode();
    }

    // Override Object#toString() so that it returns a human readable String representation.
    // It is not required by the Converter or so, it just pleases the reading in the logs.
    public String toString() {
        return "Foo[" + key + "," + value + "]";
    }

}

And here is a fake DAO which maintains the Foo DTO's. Take note that the database is simulated by a static backing map, this doesn't need to occur in real life. It should actually be mapped to a database or maybe even configuration files or so. For more information about the DAO pattern, check this article: DAO tutorial - the data layer.

package mypackage;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class FooDAO {

    // Init ---------------------------------------------------------------------------------------

    private static Map<String, Foo> fooMap;

    static {
        loadFooMap(); // Preload the fake database.
    }

    // Actions ------------------------------------------------------------------------------------

    public Foo find(String key) {
        return fooMap.get(key);
    }

    public List<Foo> list() {
        return new ArrayList<Foo>(fooMap.values());
    }

    public Map<String, Foo> map() {
        return fooMap;
    }

    // Helpers ------------------------------------------------------------------------------------

    private static void loadFooMap() {
        // This is just a fake database. We're using LinkedHashMap as it maintains the ordering.
        fooMap = new LinkedHashMap<String, Foo>();
        fooMap.put("fooKey1", new Foo("fooKey1", "fooValue1"));
        fooMap.put("fooKey2", new Foo("fooKey2", "fooValue2"));
        fooMap.put("fooKey3", new Foo("fooKey3", "fooValue3"));
    }

}

Finally here is the long-awaiting FooConverter which can be used in JSF components. It converts from Foo to String and vice versa. You can attach it to almost any HTML input or output component using the converter attribute or using the f:converter facet.

package mypackage;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class FooConverter implements Converter {

    // Init ---------------------------------------------------------------------------------------
    
    private static FooDAO fooDAO = new FooDAO();

    // Actions ------------------------------------------------------------------------------------
    
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        // Convert the unique String representation of Foo to the actual Foo object.
        return fooDAO.find(value);
    }

    public String getAsString(FacesContext context, UIComponent component, Object value) {
        // Convert the Foo object to its unique String representation.
        return ((Foo) value).getKey();
    }

}

Take care with runtime exceptions as NullPointerException, ClassCastException and maybe some exceptions which can be thrown by your DAO. Those have to be checked and/or caught and should be thrown as a new ConverterException with the appropriate message which would be shown in the attached h:message(s) tag.

The MyBean and the FooConverter are definied in the faces-config.xml as follows:

<converter>
    <converter-id>fooConverter</converter-id>
    <converter-class>mypackage.FooConverter</converter-class>
</converter>
<managed-bean>
    <managed-bean-name>myBean</managed-bean-name>
    <managed-bean-class>mypackage.MyBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
</managed-bean>

You can however also use converter-for-class instead of converter-id so that JSF will always use the specified converter class for the given class. This way you don't need to specify the f:converter in your JSF code.

<converter>
    <converter-for-class>mypackage.Foo</converter-for-class>
    <converter-class>mypackage.FooConverter</converter-class>
</converter>

That's all, folks!

Back to top

Using a backing Map

You can also decide to use a so-called backing Map to maintain String-Object pairs and convert against it in the backing bean logic. The String keys can be used in the view layer (JSF pages) and the Object values can be used in the business and data layers (backing beans and DAO's). You can also use Number-Object pairs however.

The relevant part of the JSF page is almost the same as in the case of using a converter, only the f:converter tag is removed:

<h:form>
    <h:selectOneMenu value="#{myBean.selectedItem}">
        <f:selectItems value="#{myBean.selectItems}" />
    </h:selectOneMenu>
    <h:commandButton value="submit" action="#{myBean.action}" />
    <h:messages />
</h:form>

And here is the appropriate backing bean MyBean, it has a static Map of String-Foo pairs. It's your choice how you would load/maintain it, this is just a basic example. Take note that the selectedItem is now a String instead of Foo.

package mypackage;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class MyBean {

    // Init ---------------------------------------------------------------------------------------

    private static Map<String, Foo> fooMap = new FooDAO().map();
    private List<SelectItem> selectItems;
    private String selectedItem; // You can also use any Number type, e.g. Long.

    {
        fillSelectItems();
    }

    // Actions ------------------------------------------------------------------------------------

    public void action() {
        System.out.println("Selected Foo item: " + fooMap.get(selectedItem));
    }

    // Getters ------------------------------------------------------------------------------------

    public List<SelectItem> getSelectItems() {
        return selectItems;
    }

    public String getSelectedItem() {
        return selectedItem;
    }

    // Setters ------------------------------------------------------------------------------------

    public void setSelectedItem(String selectedItem) {
        this.selectedItem = selectedItem;
    }

    // Helpers ------------------------------------------------------------------------------------

    private void fillSelectItems() {
        selectItems = new ArrayList<SelectItem>();
        for (Foo foo : fooMap.values()) {
            selectItems.add(new SelectItem(foo.getKey(), foo.getValue()));
        }
    }

}

That's it! You can just reuse the Foo DTO, the FooDAO DAO and the faces-config.xml snippet from the Using a Converter paragraph here above. No FooConverter is needed in here and you can also safely remove it from the faces-config.xml.

Back to top

Copyright - There is no copyright on the code. You can copy, change and distribute it freely. Just mentioning this site should be fair.

(C) September 2007, BalusC