org.thymeleaf.standard.serializer.StandardJavaScriptSerializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of thymeleaf Show documentation
Show all versions of thymeleaf Show documentation
Modern server-side Java template engine for both web and standalone environments
/*
* =============================================================================
*
* Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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.thymeleaf.standard.serializer;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.CharacterEscapes;
import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.exceptions.ConfigurationException;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.util.ClassLoaderUtils;
import org.thymeleaf.util.DateUtils;
import org.unbescape.json.JsonEscape;
import org.unbescape.json.JsonEscapeLevel;
import org.unbescape.json.JsonEscapeType;
/**
*
* Default implementation of the {@link IStandardJavaScriptSerializer}.
*
*
* This implementation will delegate serialization to the
* Jackson JSON processor library if it is found in the
* classpath. If not, it will default to a custom implementation that produces similar results (but is less
* flexible).
*
*
* If a Thymeleaf application uses JavaScript template processing in a significant amount of templates or
* situations, the use of Jackson (2.6+) is recommended.
*
*
* Note that, even if Jackson is present in the classpath, its usage can be prevented by means of the
* {@code useJacksonIfAvailable} constructor flag.
*
*
* @author Daniel Fernández
*
* @since 3.0.0
*
*/
public final class StandardJavaScriptSerializer implements IStandardJavaScriptSerializer {
private static final Logger logger = LoggerFactory.getLogger(StandardJavaScriptSerializer.class);
private final IStandardJavaScriptSerializer delegate;
private String computeJacksonPackageNameIfPresent() {
// We will try to know whether Jackson is present in a way that is as resilient as possible with
// dependency package renaming, so we will return the package name.
try {
final Class> objectMapperClass = ObjectMapper.class;
final String objectMapperPackageName = objectMapperClass.getPackage().getName();
return objectMapperPackageName.substring(0, objectMapperPackageName.length() - ".databind".length());
} catch (final Throwable ignored) {
// Nothing bad - simply Jackson is not in the classpath
return null;
}
}
public StandardJavaScriptSerializer(final boolean useJacksonIfAvailable) {
super();
IStandardJavaScriptSerializer newDelegate = null;
final String jacksonPrefix = (useJacksonIfAvailable? computeJacksonPackageNameIfPresent() : null);
if (jacksonPrefix != null) {
try {
newDelegate = new JacksonStandardJavaScriptSerializer(jacksonPrefix);
} catch (final Exception e) {
handleErrorLoggingOnJacksonInitialization(e);
} catch (final NoSuchMethodError e) {
handleErrorLoggingOnJacksonInitialization(e);
}
}
if (newDelegate == null) {
// Jackson could not be used, so we will use a default StandardJavaScriptSerializer (non-Jackson)
newDelegate = new DefaultStandardJavaScriptSerializer();
}
this.delegate = newDelegate;
}
public void serializeValue(final Object object, final Writer writer) {
this.delegate.serializeValue(object, writer);
}
private static final class JacksonStandardJavaScriptSerializer implements IStandardJavaScriptSerializer {
private final ObjectMapper mapper;
JacksonStandardJavaScriptSerializer(final String jacksonPrefix) {
super();
this.mapper = new ObjectMapper();
this.mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
this.mapper.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
this.mapper.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII);
this.mapper.getFactory().setCharacterEscapes(new JacksonThymeleafCharacterEscapes());
this.mapper.setDateFormat(new JacksonThymeleafISO8601DateFormat());
/*
* Now try to (conditionally) initialize support for Jackson serialization of JSR310 (java.time) objects,
* by making use of the 'jackson-datatype-jsr310' optional dependency.
*/
final Class> javaTimeModuleClass =
ClassLoaderUtils.findClass(jacksonPrefix + ".datatype.jsr310.JavaTimeModule");
if (javaTimeModuleClass != null) {
// JSR310 support for Jackson is present in classpath
try {
this.mapper.registerModule((Module)javaTimeModuleClass.newInstance());
this.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
} catch (final InstantiationException e) {
throw new ConfigurationException("Exception while trying to initialize JSR310 support for Jackson", e);
} catch (final IllegalAccessException e) {
throw new ConfigurationException("Exception while trying to initialize JSR310 support for Jackson", e);
}
}
}
public void serializeValue(final Object object, final Writer writer) {
try {
this.mapper.writeValue(writer, object);
} catch (final IOException e) {
throw new TemplateProcessingException(
"An exception was raised while trying to serialize object to JavaScript using Jackson", e);
}
}
}
/*
* This DateFormat implementation replaces the standard Jackson date serialization mechanism for ISO6801 dates,
* with the aim of making Jackson output dates in a way that is at the same time ECMAScript-valid and also
* as compatible with non-Jackson JavaScript serialization infrastructure in Thymeleaf as possible. For this:
*
* * The default Jackson behaviour of outputting all dates as GMT is disabled.
* * The default Jackson format adding timezone as '+0800' is modified, as ECMAScript requires '+08:00'
*
* On the latter point, see https://github.com/FasterXML/jackson-databind/issues/1020
*/
private static final class JacksonThymeleafISO8601DateFormat extends DateFormat {
private static final long serialVersionUID = 1354081220093875129L;
/*
* This SimpleDateFormat defines an almost-ISO8601 formatter.
*
* The correct ISO8601 format would be "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", but the "X" pattern (which outputs the
* timezone as "+02:00" or "Z" instead of "+0200") was not added until Java SE 7. So the use of this
* SimpleDateFormat object requires additional post-processing.
*
* SimpleDateFormat objects are NOT thread-safe, but it is here being used from another DateFormat
* implementation, so we must suppose that it is the use of this DateFormat wrapper that will be
* adequately synchronized by Jackson.
*/
private SimpleDateFormat dateFormat;
JacksonThymeleafISO8601DateFormat() {
super();
this.dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZZZ");
setCalendar(this.dateFormat.getCalendar());
setNumberFormat(this.dateFormat.getNumberFormat());
}
@Override
public StringBuffer format(final Date date, final StringBuffer toAppendTo, final FieldPosition fieldPosition) {
final StringBuffer formatted = this.dateFormat.format(date, toAppendTo, fieldPosition);
formatted.insert(26, ':');
return formatted;
}
@Override
public Date parse(final String source, final ParsePosition pos) {
throw new UnsupportedOperationException(
"JacksonThymeleafISO8601DateFormat should never be asked for a 'parse' operation");
}
@Override
public Object clone() {
JacksonThymeleafISO8601DateFormat other = (JacksonThymeleafISO8601DateFormat) super.clone();
other.dateFormat = (SimpleDateFormat) dateFormat.clone();
return other;
}
}
/*
* This CharacterEscapes implementation makes sure that the slash ('/') and ampersand ('&') characters
* are also escaped, which is not standard Jackson behaviour.
*
* Escaping '/' covers against the possible premature closing of