All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.thymeleaf.standard.serializer.StandardJavaScriptSerializer Maven / Gradle / Ivy

Go to download

Modern server-side Java template engine for both web and standalone environments

There is a newer version: 3.0.12.2
Show newest version
/*
 * =============================================================================
 *
 *   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