| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.net.ftp.parser; |
| |
| import java.text.DateFormatSymbols; |
| import java.text.ParseException; |
| import java.text.ParsePosition; |
| import java.text.SimpleDateFormat; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.TimeZone; |
| |
| import org.apache.commons.net.ftp.Configurable; |
| import org.apache.commons.net.ftp.FTPClientConfig; |
| |
| /** |
| * Default implementation of the {@link FTPTimestampParser FTPTimestampParser} |
| * interface also implements the {@link org.apache.commons.net.ftp.Configurable Configurable} |
| * interface to allow the parsing to be configured from the outside. |
| * |
| * @see ConfigurableFTPFileEntryParserImpl |
| * @since 1.4 |
| */ |
| public class FTPTimestampParserImpl implements |
| FTPTimestampParser, Configurable |
| { |
| |
| |
| /* |
| * List of units in order of increasing significance. |
| * This allows the code to clear all units in the Calendar until it |
| * reaches the least significant unit in the parse string. |
| * The date formats are analysed to find the least significant |
| * unit (e.g. Minutes or Milliseconds) and the appropriate index to |
| * the array is saved. |
| * This is done by searching the array for the unit specifier, |
| * and returning the index. When clearing the Calendar units, |
| * the code loops through the array until the previous entry. |
| * e.g. for MINUTE it would clear MILLISECOND and SECOND |
| */ |
| private static final int[] CALENDAR_UNITS = { |
| Calendar.MILLISECOND, |
| Calendar.SECOND, |
| Calendar.MINUTE, |
| Calendar.HOUR_OF_DAY, |
| Calendar.DAY_OF_MONTH, |
| Calendar.MONTH, |
| Calendar.YEAR}; |
| /* |
| * Return the index to the array representing the least significant |
| * unit found in the date format. |
| * Default is 0 (to avoid dropping precision) |
| */ |
| private static int getEntry(final SimpleDateFormat dateFormat) { |
| if (dateFormat == null) { |
| return 0; |
| } |
| final String FORMAT_CHARS="SsmHdM"; |
| final String pattern = dateFormat.toPattern(); |
| for(final char ch : FORMAT_CHARS.toCharArray()) { |
| if (pattern.indexOf(ch) != -1){ // found the character |
| switch(ch) { |
| case 'S': |
| return indexOf(Calendar.MILLISECOND); |
| case 's': |
| return indexOf(Calendar.SECOND); |
| case 'm': |
| return indexOf(Calendar.MINUTE); |
| case 'H': |
| return indexOf(Calendar.HOUR_OF_DAY); |
| case 'd': |
| return indexOf(Calendar.DAY_OF_MONTH); |
| case 'M': |
| return indexOf(Calendar.MONTH); |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /* |
| * Find the entry in the CALENDAR_UNITS array. |
| */ |
| private static int indexOf(final int calendarUnit) { |
| int i; |
| for(i = 0; i <CALENDAR_UNITS.length; i++) { |
| if (calendarUnit == CALENDAR_UNITS[i]) { |
| return i; |
| } |
| } |
| return 0; |
| } |
| /* |
| * Sets the Calendar precision (used by FTPFile#toFormattedDate) by clearing |
| * the immediately preceeding unit (if any). |
| * Unfortunately the clear(int) method results in setting all other units. |
| */ |
| private static void setPrecision(final int index, final Calendar working) { |
| if (index <= 0) { // e.g. MILLISECONDS |
| return; |
| } |
| final int field = CALENDAR_UNITS[index-1]; |
| // Just in case the analysis is wrong, stop clearing if |
| // field value is not the default. |
| final int value = working.get(field); |
| if (value != 0) { // don't reset if it has a value |
| // new Throwable("Unexpected value "+value).printStackTrace(); // DEBUG |
| } else { |
| working.clear(field); // reset just the required field |
| } |
| } |
| |
| /** The date format for all dates, except possibly recent dates. Assumed to include the year. */ |
| private SimpleDateFormat defaultDateFormat; |
| |
| /* The index in CALENDAR_UNITS of the smallest time unit in defaultDateFormat */ |
| private int defaultDateSmallestUnitIndex; |
| |
| /** The format used for recent dates (which don't have the year). May be null. */ |
| private SimpleDateFormat recentDateFormat; |
| |
| /* The index in CALENDAR_UNITS of the smallest time unit in recentDateFormat */ |
| private int recentDateSmallestUnitIndex; |
| |
| private boolean lenientFutureDates; |
| |
| /** |
| * The only constructor for this class. |
| */ |
| public FTPTimestampParserImpl() { |
| setDefaultDateFormat(DEFAULT_SDF, null); |
| setRecentDateFormat(DEFAULT_RECENT_SDF, null); |
| } |
| |
| /** |
| * Implementation of the {@link Configurable Configurable} |
| * interface. Configures this <code>FTPTimestampParser</code> according |
| * to the following logic: |
| * <p> |
| * Set up the {@link FTPClientConfig#setDefaultDateFormatStr(java.lang.String) defaultDateFormat} |
| * and optionally the {@link FTPClientConfig#setRecentDateFormatStr(String) recentDateFormat} |
| * to values supplied in the config based on month names configured as follows: |
| * </p> |
| * <ul> |
| * <li>If a {@link FTPClientConfig#setShortMonthNames(String) shortMonthString} |
| * has been supplied in the <code>config</code>, use that to parse parse timestamps.</li> |
| * <li>Otherwise, if a {@link FTPClientConfig#setServerLanguageCode(String) serverLanguageCode} |
| * has been supplied in the <code>config</code>, use the month names represented |
| * by that {@link FTPClientConfig#lookupDateFormatSymbols(String) language} |
| * to parse timestamps.</li> |
| * <li>otherwise use default English month names</li> |
| * </ul><p> |
| * Finally if a {@link org.apache.commons.net.ftp.FTPClientConfig#setServerTimeZoneId(String) serverTimeZoneId} |
| * has been supplied via the config, set that into all date formats that have |
| * been configured. |
| * </p> |
| */ |
| @Override |
| public void configure(final FTPClientConfig config) { |
| DateFormatSymbols dfs = null; |
| |
| final String languageCode = config.getServerLanguageCode(); |
| final String shortmonths = config.getShortMonthNames(); |
| if (shortmonths != null) { |
| dfs = FTPClientConfig.getDateFormatSymbols(shortmonths); |
| } else if (languageCode != null) { |
| dfs = FTPClientConfig.lookupDateFormatSymbols(languageCode); |
| } else { |
| dfs = FTPClientConfig.lookupDateFormatSymbols("en"); |
| } |
| |
| |
| final String recentFormatString = config.getRecentDateFormatStr(); |
| setRecentDateFormat(recentFormatString, dfs); |
| |
| final String defaultFormatString = config.getDefaultDateFormatStr(); |
| if (defaultFormatString == null) { |
| throw new IllegalArgumentException("defaultFormatString cannot be null"); |
| } |
| setDefaultDateFormat(defaultFormatString, dfs); |
| |
| setServerTimeZone(config.getServerTimeZoneId()); |
| |
| this.lenientFutureDates = config.isLenientFutureDates(); |
| } |
| |
| /** |
| * @return Returns the defaultDateFormat. |
| */ |
| public SimpleDateFormat getDefaultDateFormat() { |
| return defaultDateFormat; |
| } |
| |
| /** |
| * @return Returns the defaultDateFormat pattern string. |
| */ |
| public String getDefaultDateFormatString() { |
| return defaultDateFormat.toPattern(); |
| } |
| /** |
| * @return Returns the recentDateFormat. |
| */ |
| public SimpleDateFormat getRecentDateFormat() { |
| return recentDateFormat; |
| } |
| /** |
| * @return Returns the recentDateFormat. |
| */ |
| public String getRecentDateFormatString() { |
| return recentDateFormat.toPattern(); |
| } |
| /** |
| * @return Returns the serverTimeZone used by this parser. |
| */ |
| public TimeZone getServerTimeZone() { |
| return this.defaultDateFormat.getTimeZone(); |
| } |
| /** |
| * @return returns an array of 12 strings representing the short |
| * month names used by this parse. |
| */ |
| public String[] getShortMonths() { |
| return defaultDateFormat.getDateFormatSymbols().getShortMonths(); |
| } |
| /** |
| * @return Returns the lenientFutureDates. |
| */ |
| boolean isLenientFutureDates() { |
| return lenientFutureDates; |
| } |
| |
| /** |
| * Implements the one {@link FTPTimestampParser#parseTimestamp(String) method} |
| * in the {@link FTPTimestampParser FTPTimestampParser} interface |
| * according to this algorithm: |
| * |
| * If the recentDateFormat member has been defined, try to parse the |
| * supplied string with that. If that parse fails, or if the recentDateFormat |
| * member has not been defined, attempt to parse with the defaultDateFormat |
| * member. If that fails, throw a ParseException. |
| * |
| * This method assumes that the server time is the same as the local time. |
| * |
| * @see FTPTimestampParserImpl#parseTimestamp(String, Calendar) |
| * |
| * @param timestampStr The timestamp to be parsed |
| * @return a Calendar with the parsed timestamp |
| */ |
| @Override |
| public Calendar parseTimestamp(final String timestampStr) throws ParseException { |
| final Calendar now = Calendar.getInstance(); |
| return parseTimestamp(timestampStr, now); |
| } |
| |
| |
| /** |
| * If the recentDateFormat member has been defined, try to parse the |
| * supplied string with that. If that parse fails, or if the recentDateFormat |
| * member has not been defined, attempt to parse with the defaultDateFormat |
| * member. If that fails, throw a ParseException. |
| * |
| * This method allows a {@link Calendar} instance to be passed in which represents the |
| * current (system) time. |
| * |
| * @see FTPTimestampParser#parseTimestamp(String) |
| * @param timestampStr The timestamp to be parsed |
| * @param serverTime The current time for the server |
| * @return the calendar |
| * @throws ParseException if timestamp cannot be parsed |
| * @since 1.5 |
| */ |
| public Calendar parseTimestamp(final String timestampStr, final Calendar serverTime) throws ParseException { |
| final Calendar working = (Calendar) serverTime.clone(); |
| working.setTimeZone(getServerTimeZone()); // is this needed? |
| |
| Date parsed = null; |
| |
| if (recentDateFormat != null) { |
| final Calendar now = (Calendar) serverTime.clone();// Copy this, because we may change it |
| now.setTimeZone(this.getServerTimeZone()); |
| if (lenientFutureDates) { |
| // add a day to "now" so that "slop" doesn't cause a date |
| // slightly in the future to roll back a full year. (Bug 35181 => NET-83) |
| now.add(Calendar.DAY_OF_MONTH, 1); |
| } |
| // The Java SimpleDateFormat class uses the epoch year 1970 if not present in the input |
| // As 1970 was not a leap year, it cannot parse "Feb 29" correctly. |
| // Java 1.5+ returns Mar 1 1970 |
| // Temporarily add the current year to the short date time |
| // to cope with short-date leap year strings. |
| // Since Feb 29 is more that 6 months from the end of the year, this should be OK for |
| // all instances of short dates which are +- 6 months from current date. |
| // TODO this won't always work for systems that use short dates +0/-12months |
| // e.g. if today is Jan 1 2001 and the short date is Feb 29 |
| final String year = Integer.toString(now.get(Calendar.YEAR)); |
| final String timeStampStrPlusYear = timestampStr + " " + year; |
| final SimpleDateFormat hackFormatter = new SimpleDateFormat(recentDateFormat.toPattern() + " yyyy", |
| recentDateFormat.getDateFormatSymbols()); |
| hackFormatter.setLenient(false); |
| hackFormatter.setTimeZone(recentDateFormat.getTimeZone()); |
| final ParsePosition pp = new ParsePosition(0); |
| parsed = hackFormatter.parse(timeStampStrPlusYear, pp); |
| // Check if we parsed the full string, if so it must have been a short date originally |
| if (parsed != null && pp.getIndex() == timeStampStrPlusYear.length()) { |
| working.setTime(parsed); |
| if (working.after(now)) { // must have been last year instead |
| working.add(Calendar.YEAR, -1); |
| } |
| setPrecision(recentDateSmallestUnitIndex, working); |
| return working; |
| } |
| } |
| |
| final ParsePosition pp = new ParsePosition(0); |
| parsed = defaultDateFormat.parse(timestampStr, pp); |
| // note, length checks are mandatory for us since |
| // SimpleDateFormat methods will succeed if less than |
| // full string is matched. They will also accept, |
| // despite "leniency" setting, a two-digit number as |
| // a valid year (e.g. 22:04 will parse as 22 A.D.) |
| // so could mistakenly confuse an hour with a year, |
| // if we don't insist on full length parsing. |
| if ((parsed == null) || (pp.getIndex() != timestampStr.length())) { |
| throw new ParseException( |
| "Timestamp '"+timestampStr+"' could not be parsed using a server time of " |
| +serverTime.getTime().toString(), |
| pp.getErrorIndex()); |
| } |
| working.setTime(parsed); |
| setPrecision(defaultDateSmallestUnitIndex, working); |
| return working; |
| } |
| /** |
| * @param format The defaultDateFormat to be set. |
| * @param dfs the symbols to use (may be null) |
| */ |
| private void setDefaultDateFormat(final String format, final DateFormatSymbols dfs) { |
| if (format != null) { |
| if (dfs != null) { |
| this.defaultDateFormat = new SimpleDateFormat(format, dfs); |
| } else { |
| this.defaultDateFormat = new SimpleDateFormat(format); |
| } |
| this.defaultDateFormat.setLenient(false); |
| } else { |
| this.defaultDateFormat = null; |
| } |
| this.defaultDateSmallestUnitIndex = getEntry(this.defaultDateFormat); |
| } |
| |
| /** |
| * @param lenientFutureDates The lenientFutureDates to set. |
| */ |
| void setLenientFutureDates(final boolean lenientFutureDates) { |
| this.lenientFutureDates = lenientFutureDates; |
| } |
| /** |
| * @param format The recentDateFormat to set. |
| * @param dfs the symbols to use (may be null) |
| */ |
| private void setRecentDateFormat(final String format, final DateFormatSymbols dfs) { |
| if (format != null) { |
| if (dfs != null) { |
| this.recentDateFormat = new SimpleDateFormat(format, dfs); |
| } else { |
| this.recentDateFormat = new SimpleDateFormat(format); |
| } |
| this.recentDateFormat.setLenient(false); |
| } else { |
| this.recentDateFormat = null; |
| } |
| this.recentDateSmallestUnitIndex = getEntry(this.recentDateFormat); |
| } |
| /** |
| * sets a TimeZone represented by the supplied ID string into all |
| * of the parsers used by this server. |
| * @param serverTimeZoneId Time Id java.util.TimeZone id used by |
| * the ftp server. If null the client's local time zone is assumed. |
| */ |
| private void setServerTimeZone(final String serverTimeZoneId) { |
| TimeZone serverTimeZone = TimeZone.getDefault(); |
| if (serverTimeZoneId != null) { |
| serverTimeZone = TimeZone.getTimeZone(serverTimeZoneId); |
| } |
| this.defaultDateFormat.setTimeZone(serverTimeZone); |
| if (this.recentDateFormat != null) { |
| this.recentDateFormat.setTimeZone(serverTimeZone); |
| } |
| } |
| } |