blob: f98df2031342e128b3bf9ced1ff878cb0d9774af [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 org.apache.sling.scripting.sightly.impl.engine.extension;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.LocaleUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.scripting.sightly.SightlyException;
import org.apache.sling.scripting.sightly.extension.RuntimeExtension;
import org.apache.sling.scripting.sightly.render.RenderContext;
import org.apache.sling.scripting.sightly.render.RuntimeObjectModel;
import org.osgi.service.component.annotations.Component;
@Component(
service = RuntimeExtension.class,
property = {
RuntimeExtension.NAME + "=" + RuntimeExtension.FORMAT
}
)
public class FormatFilterExtension implements RuntimeExtension {
private static final Pattern PLACEHOLDER_REGEX = Pattern.compile("\\{\\d+}");
private static final String FORMAT_OPTION = "format";
private static final String TYPE_OPTION = "type";
private static final String LOCALE_OPTION = "locale";
private static final String TIMEZONE_OPTION = "timezone";
private static final String DATE_FORMAT_TYPE = "date";
private static final String NUMBER_FORMAT_TYPE = "number";
private static final String STRING_FORMAT_TYPE = "string";
@Override
public Object call(final RenderContext renderContext, Object... arguments) {
ExtensionUtils.checkArgumentCount(RuntimeExtension.FORMAT, arguments, 2);
RuntimeObjectModel runtimeObjectModel = renderContext.getObjectModel();
String source = runtimeObjectModel.toString(arguments[0]);
Map<String, Object> options = (Map<String, Object>) arguments[1];
String formattingType = runtimeObjectModel.toString(options.get(TYPE_OPTION));
Object formatObject = options.get(FORMAT_OPTION);
boolean hasPlaceHolders = PLACEHOLDER_REGEX.matcher(source).find();
if (STRING_FORMAT_TYPE.equals(formattingType)) {
return getFormattedString(runtimeObjectModel, source, formatObject);
} else if (DATE_FORMAT_TYPE.equals(formattingType) || (!hasPlaceHolders && runtimeObjectModel.isDate(formatObject))) {
return getDateFormattedString(runtimeObjectModel, source, options, formatObject);
} else if (NUMBER_FORMAT_TYPE.equals(formattingType) || (!hasPlaceHolders && runtimeObjectModel.isNumber(formatObject))) {
return getNumberFormattedString(runtimeObjectModel, source, options, formatObject);
}
if (hasPlaceHolders) {
return getFormattedString(runtimeObjectModel, source, formatObject);
}
try {
// somebody will hate me for this
new SimpleDateFormat(source);
return getDateFormattedString(runtimeObjectModel, source, options, formatObject);
} catch (IllegalArgumentException e) {
// ignore
}
try {
// for this too, but such is life
new DecimalFormat(source);
return getNumberFormattedString(runtimeObjectModel, source, options, formatObject);
} catch (IllegalArgumentException e) {
// ignore
}
return getFormattedString(runtimeObjectModel, source, formatObject);
}
private Object getFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Object formatObject) {
Object[] params = decodeParams(runtimeObjectModel, formatObject);
return formatString(runtimeObjectModel, source, params);
}
private String getNumberFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Map<String, Object> options,
Object formatObject) {
Locale locale = getLocale(runtimeObjectModel, options);
return formatNumber(source, runtimeObjectModel.toNumber(formatObject), locale);
}
private String getDateFormattedString(RuntimeObjectModel runtimeObjectModel, String source, Map<String, Object> options,
Object formatObject) {
Locale locale = getLocale(runtimeObjectModel, options);
TimeZone timezone = getTimezone(runtimeObjectModel, options);
return formatDate(source, runtimeObjectModel.toDate(formatObject), locale, timezone);
}
private Locale getLocale(RuntimeObjectModel runtimeObjectModel, Map<String, Object> options) {
String localeOption = null;
if (options.containsKey(LOCALE_OPTION)) {
localeOption = runtimeObjectModel.toString(options.get(LOCALE_OPTION));
}
if (StringUtils.isNotBlank(localeOption)) {
return LocaleUtils.toLocale(localeOption);
}
return null;
}
private TimeZone getTimezone(RuntimeObjectModel runtimeObjectModel, Map<String, Object> options) {
if ( options.containsKey(TIMEZONE_OPTION)) {
return TimeZone.getTimeZone(runtimeObjectModel.toString(options.get(TIMEZONE_OPTION)));
} else {
Object formatObject = options.get(FORMAT_OPTION);
if (formatObject instanceof Calendar) {
return ((Calendar)formatObject).getTimeZone();
}
return TimeZone.getDefault();
}
}
private Object[] decodeParams(RuntimeObjectModel runtimeObjectModel, Object paramObj) {
if (paramObj == null) {
return null;
}
if (runtimeObjectModel.isCollection(paramObj)) {
return runtimeObjectModel.toCollection(paramObj).toArray();
}
return new Object[] {paramObj};
}
private String formatString(RuntimeObjectModel runtimeObjectModel, String source, Object[] params) {
if (params == null) {
return null;
}
Matcher matcher = PLACEHOLDER_REGEX.matcher(source);
StringBuilder builder = new StringBuilder();
int lastPos = 0;
boolean matched = true;
while (matched) {
matched = matcher.find();
if (matched) {
String group = matcher.group();
int paramIndex = Integer.parseInt(group.substring(1, group.length() - 1));
String replacement = toString(runtimeObjectModel, params, paramIndex);
int matchStart = matcher.start();
int matchEnd = matcher.end();
builder.append(source, lastPos, matchStart).append(replacement);
lastPos = matchEnd;
}
}
builder.append(source, lastPos, source.length());
return builder.toString();
}
private String toString(RuntimeObjectModel runtimeObjectModel, Object[] params, int index) {
if (index >= 0 && index < params.length) {
return runtimeObjectModel.toString(params[index]);
}
return "";
}
private String formatDate(String format, Date date, Locale locale, TimeZone timezone) {
if (date == null) {
return null;
}
try {
SimpleDateFormat formatter;
if (locale != null) {
formatter = new SimpleDateFormat(format, locale);
} else {
formatter = new SimpleDateFormat(format);
}
if (timezone != null) {
formatter.setTimeZone(timezone);
}
return formatter.format(date);
} catch (Exception e) {
String error = String.format("Error during formatting of date %s with format %s, locale %s and timezone %s", date, format, locale, timezone);
throw new SightlyException( error, e);
}
}
private String formatNumber(String format, Number number, Locale locale) {
if (number == null) {
return null;
}
try {
NumberFormat formatter;
if (locale != null) {
formatter = new DecimalFormat(format, new DecimalFormatSymbols(locale));
} else {
formatter = new DecimalFormat(format);
}
return formatter.format(number);
} catch (Exception e) {
String error = String.format("Error during formatting of number %s with format %s and locale %s", number, format, locale);
throw new SightlyException( error, e);
}
}
}