blob: 690fd0fadb2ac70d7a8d336e9ecc3bd0d759933a [file] [log] [blame]
/*
* 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 freemarker.core;
import java.util.Date;
import java.util.TimeZone;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.DateUtil;
import freemarker.template.utility.DateUtil.CalendarFieldsToDateConverter;
import freemarker.template.utility.DateUtil.DateParseException;
import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.StringUtil;
abstract class ISOLikeTemplateDateFormat extends TemplateDateFormat {
private static final String XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE
= "Less than seconds accuracy isn't allowed by the XML Schema format";
private final ISOLikeTemplateDateFormatFactory factory;
private final Environment env;
protected final int dateType;
protected final boolean zonelessInput;
protected final TimeZone timeZone;
protected final Boolean forceUTC;
protected final Boolean showZoneOffset;
protected final int accuracy;
/**
* @param formatString The value of the ..._format setting, like "iso nz".
* @param parsingStart The index of the char in the {@code settingValue} that directly after the prefix that has
* indicated the exact formatter class (like "iso" or "xs")
*/
public ISOLikeTemplateDateFormat(
final String formatString, int parsingStart,
int dateType, boolean zonelessInput,
TimeZone timeZone,
ISOLikeTemplateDateFormatFactory factory, Environment env)
throws InvalidFormatParametersException, UnknownDateTypeFormattingUnsupportedException {
this.factory = factory;
this.env = env;
if (dateType == TemplateDateModel.UNKNOWN) {
throw new UnknownDateTypeFormattingUnsupportedException();
}
this.dateType = dateType;
this.zonelessInput = zonelessInput;
final int ln = formatString.length();
boolean afterSeparator = false;
int i = parsingStart;
int accuracy = DateUtil.ACCURACY_MILLISECONDS;
Boolean showZoneOffset = null;
Boolean forceUTC = Boolean.FALSE;
while (i < ln) {
final char c = formatString.charAt(i++);
if (c == '_' || c == ' ') {
afterSeparator = true;
} else {
if (!afterSeparator) {
throw new InvalidFormatParametersException(
"Missing space or \"_\" before \"" + c + "\" (at char pos. " + i + ").");
}
switch (c) {
case 'h':
case 'm':
case 's':
if (accuracy != DateUtil.ACCURACY_MILLISECONDS) {
throw new InvalidFormatParametersException(
"Character \"" + c + "\" is unexpected as accuracy was already specified earlier "
+ "(at char pos. " + i + ").");
}
switch (c) {
case 'h':
if (isXSMode()) {
throw new InvalidFormatParametersException(
XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
}
accuracy = DateUtil.ACCURACY_HOURS;
break;
case 'm':
if (i < ln && formatString.charAt(i) == 's') {
i++;
accuracy = DateUtil.ACCURACY_MILLISECONDS_FORCED;
} else {
if (isXSMode()) {
throw new InvalidFormatParametersException(
XS_LESS_THAN_SECONDS_ACCURACY_ERROR_MESSAGE);
}
accuracy = DateUtil.ACCURACY_MINUTES;
}
break;
case 's':
accuracy = DateUtil.ACCURACY_SECONDS;
break;
}
break;
case 'f':
if (i < ln && formatString.charAt(i) == 'u') {
checkForceUTCNotSet(forceUTC);
i++;
forceUTC = Boolean.TRUE;
break;
}
// Falls through
case 'n':
if (showZoneOffset != null) {
throw new InvalidFormatParametersException(
"Character \"" + c + "\" is unexpected as zone offset visibility was already "
+ "specified earlier. (at char pos. " + i + ").");
}
switch (c) {
case 'n':
if (i < ln && formatString.charAt(i) == 'z') {
i++;
showZoneOffset = Boolean.FALSE;
} else {
throw new InvalidFormatParametersException(
"\"n\" must be followed by \"z\" (at char pos. " + i + ").");
}
break;
case 'f':
if (i < ln && formatString.charAt(i) == 'z') {
i++;
showZoneOffset = Boolean.TRUE;
} else {
throw new InvalidFormatParametersException(
"\"f\" must be followed by \"z\" (at char pos. " + i + ").");
}
break;
}
break;
case 'u':
checkForceUTCNotSet(forceUTC);
forceUTC = null; // means UTC will be used except for zonelessInput
break;
default:
throw new InvalidFormatParametersException(
"Unexpected character, " + StringUtil.jQuote(String.valueOf(c))
+ ". Expected the beginning of one of: h, m, s, ms, nz, fz, u"
+ " (at char pos. " + i + ").");
} // switch
afterSeparator = false;
} // else
} // while
this.accuracy = accuracy;
this.showZoneOffset = showZoneOffset;
this.forceUTC = forceUTC;
this.timeZone = timeZone;
}
private void checkForceUTCNotSet(Boolean fourceUTC) throws InvalidFormatParametersException {
if (fourceUTC != Boolean.FALSE) {
throw new InvalidFormatParametersException(
"The UTC usage option was already set earlier.");
}
}
@Override
public final String formatToPlainText(TemplateDateModel dateModel) throws TemplateModelException {
final Date date = TemplateFormatUtil.getNonNullDate(dateModel);
return format(
date,
dateType != TemplateDateModel.TIME,
dateType != TemplateDateModel.DATE,
showZoneOffset == null
? !zonelessInput
: showZoneOffset.booleanValue(),
accuracy,
(forceUTC == null ? !zonelessInput : forceUTC.booleanValue()) ? DateUtil.UTC : timeZone,
factory.getISOBuiltInCalendar(env));
}
protected abstract String format(Date date,
boolean datePart, boolean timePart, boolean offsetPart,
int accuracy,
TimeZone timeZone,
DateToISO8601CalendarFactory calendarFactory);
@Override
@SuppressFBWarnings(value = "RC_REF_COMPARISON_BAD_PRACTICE_BOOLEAN",
justification = "Known to use the singleton Boolean-s only")
public final Date parse(String s, int dateType) throws UnparsableValueException {
CalendarFieldsToDateConverter calToDateConverter = factory.getCalendarFieldsToDateCalculator(env);
TimeZone tz = forceUTC != Boolean.FALSE ? DateUtil.UTC : timeZone;
try {
if (dateType == TemplateDateModel.DATE) {
return parseDate(s, tz, calToDateConverter);
} else if (dateType == TemplateDateModel.TIME) {
return parseTime(s, tz, calToDateConverter);
} else if (dateType == TemplateDateModel.DATETIME) {
return parseDateTime(s, tz, calToDateConverter);
} else {
throw new BugException("Unexpected date type: " + dateType);
}
} catch (DateParseException e) {
throw new UnparsableValueException(e.getMessage(), e);
}
}
protected abstract Date parseDate(
String s, TimeZone tz,
CalendarFieldsToDateConverter calToDateConverter)
throws DateParseException;
protected abstract Date parseTime(
String s, TimeZone tz,
CalendarFieldsToDateConverter calToDateConverter)
throws DateParseException;
protected abstract Date parseDateTime(
String s, TimeZone tz,
CalendarFieldsToDateConverter calToDateConverter)
throws DateParseException;
@Override
public final String getDescription() {
switch (dateType) {
case TemplateDateModel.DATE: return getDateDescription();
case TemplateDateModel.TIME: return getTimeDescription();
case TemplateDateModel.DATETIME: return getDateTimeDescription();
default: return "<error: wrong format dateType>";
}
}
protected abstract String getDateDescription();
protected abstract String getTimeDescription();
protected abstract String getDateTimeDescription();
@Override
public final boolean isLocaleBound() {
return false;
}
@Override
public boolean isTimeZoneBound() {
return true;
}
protected abstract boolean isXSMode();
}