1 package com.fasterxml.jackson.databind.ser.std; 2 3 import java.io.IOException; 4 import java.lang.reflect.Type; 5 import java.text.DateFormat; 6 import java.text.SimpleDateFormat; 7 import java.util.Date; 8 import java.util.Locale; 9 import java.util.TimeZone; 10 import java.util.concurrent.atomic.AtomicReference; 11 12 import com.fasterxml.jackson.annotation.JsonFormat; 13 14 import com.fasterxml.jackson.core.JsonGenerator; 15 import com.fasterxml.jackson.core.JsonParser; 16 17 import com.fasterxml.jackson.databind.*; 18 import com.fasterxml.jackson.databind.jsonFormatVisitors.*; 19 import com.fasterxml.jackson.databind.ser.ContextualSerializer; 20 import com.fasterxml.jackson.databind.util.StdDateFormat; 21 22 @SuppressWarnings("serial") 23 public abstract class DateTimeSerializerBase<T> 24 extends StdScalarSerializer<T> 25 implements ContextualSerializer 26 { 27 /** 28 * Flag that indicates that serialization must be done as the 29 * Java timestamp, regardless of other settings. 30 */ 31 protected final Boolean _useTimestamp; 32 33 /** 34 * Specific format to use, if not default format: non null value 35 * also indicates that serialization is to be done as JSON String, 36 * not numeric timestamp, unless {@link #_useTimestamp} is true. 37 */ 38 protected final DateFormat _customFormat; 39 40 /** 41 * If {@link #_customFormat} is used, we will try to reuse instances in simplest 42 * possible form; thread-safe, but without overhead of <code>ThreadLocal</code> 43 * (not from code, but wrt retaining of possibly large number of format instances 44 * over all threads, properties with custom formats). 45 * 46 * @since 2.9 47 */ 48 protected final AtomicReference<DateFormat> _reusedCustomFormat; 49 DateTimeSerializerBase(Class<T> type, Boolean useTimestamp, DateFormat customFormat)50 protected DateTimeSerializerBase(Class<T> type, 51 Boolean useTimestamp, DateFormat customFormat) 52 { 53 super(type); 54 _useTimestamp = useTimestamp; 55 _customFormat = customFormat; 56 _reusedCustomFormat = (customFormat == null) ? null : new AtomicReference<DateFormat>(); 57 } 58 withFormat(Boolean timestamp, DateFormat customFormat)59 public abstract DateTimeSerializerBase<T> withFormat(Boolean timestamp, DateFormat customFormat); 60 61 @Override createContextual(SerializerProvider serializers, BeanProperty property)62 public JsonSerializer<?> createContextual(SerializerProvider serializers, 63 BeanProperty property) throws JsonMappingException 64 { 65 // Note! Should not skip if `property` null since that'd skip check 66 // for config overrides, in case of root value 67 JsonFormat.Value format = findFormatOverrides(serializers, property, handledType()); 68 if (format == null) { 69 return this; 70 } 71 // Simple case first: serialize as numeric timestamp? 72 JsonFormat.Shape shape = format.getShape(); 73 if (shape.isNumeric()) { 74 return withFormat(Boolean.TRUE, null); 75 } 76 77 // 08-Jun-2017, tatu: With [databind#1648], this gets bit tricky.. 78 // First: custom pattern will override things 79 if (format.hasPattern()) { 80 final Locale loc = format.hasLocale() 81 ? format.getLocale() 82 : serializers.getLocale(); 83 SimpleDateFormat df = new SimpleDateFormat(format.getPattern(), loc); 84 TimeZone tz = format.hasTimeZone() ? format.getTimeZone() 85 : serializers.getTimeZone(); 86 df.setTimeZone(tz); 87 return withFormat(Boolean.FALSE, df); 88 } 89 90 // Otherwise, need one of these changes: 91 final boolean hasLocale = format.hasLocale(); 92 final boolean hasTZ = format.hasTimeZone(); 93 final boolean asString = (shape == JsonFormat.Shape.STRING); 94 95 if (!hasLocale && !hasTZ && !asString) { 96 return this; 97 } 98 99 DateFormat df0 = serializers.getConfig().getDateFormat(); 100 // Jackson's own `StdDateFormat` is quite easy to deal with... 101 if (df0 instanceof StdDateFormat) { 102 StdDateFormat std = (StdDateFormat) df0; 103 if (format.hasLocale()) { 104 std = std.withLocale(format.getLocale()); 105 } 106 if (format.hasTimeZone()) { 107 std = std.withTimeZone(format.getTimeZone()); 108 } 109 return withFormat(Boolean.FALSE, std); 110 } 111 112 // 08-Jun-2017, tatu: Unfortunately there's no generally usable 113 // mechanism for changing `DateFormat` instances (or even clone()ing) 114 // So: require it be `SimpleDateFormat`; can't config other types 115 if (!(df0 instanceof SimpleDateFormat)) { 116 serializers.reportBadDefinition(handledType(), String.format( 117 "Configured `DateFormat` (%s) not a `SimpleDateFormat`; cannot configure `Locale` or `TimeZone`", 118 df0.getClass().getName())); 119 } 120 SimpleDateFormat df = (SimpleDateFormat) df0; 121 if (hasLocale) { 122 // Ugh. No way to change `Locale`, create copy; must re-crete completely: 123 df = new SimpleDateFormat(df.toPattern(), format.getLocale()); 124 } else { 125 df = (SimpleDateFormat) df.clone(); 126 } 127 TimeZone newTz = format.getTimeZone(); 128 boolean changeTZ = (newTz != null) && !newTz.equals(df.getTimeZone()); 129 if (changeTZ) { 130 df.setTimeZone(newTz); 131 } 132 return withFormat(Boolean.FALSE, df); 133 } 134 135 /* 136 /********************************************************** 137 /* Accessors 138 /********************************************************** 139 */ 140 141 @Override isEmpty(SerializerProvider serializers, T value)142 public boolean isEmpty(SerializerProvider serializers, T value) { 143 // 09-Mar-2017, tatu: as per [databind#1550] timestamp 0 is NOT "empty"; but 144 // with versions up to 2.8.x this was the case. Fixed for 2.9. 145 // return _timestamp(value) == 0L; 146 return false; 147 } 148 _timestamp(T value)149 protected abstract long _timestamp(T value); 150 151 @Override getSchema(SerializerProvider serializers, Type typeHint)152 public JsonNode getSchema(SerializerProvider serializers, Type typeHint) { 153 //todo: (ryan) add a format for the date in the schema? 154 return createSchemaNode(_asTimestamp(serializers) ? "number" : "string", true); 155 } 156 157 @Override acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)158 public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException 159 { 160 _acceptJsonFormatVisitor(visitor, typeHint, _asTimestamp(visitor.getProvider())); 161 } 162 163 /* 164 /********************************************************** 165 /* Actual serialization 166 /********************************************************** 167 */ 168 169 @Override serialize(T value, JsonGenerator gen, SerializerProvider serializers)170 public abstract void serialize(T value, JsonGenerator gen, SerializerProvider serializers) 171 throws IOException; 172 173 /* 174 /********************************************************** 175 /* Helper methods 176 /********************************************************** 177 */ 178 _asTimestamp(SerializerProvider serializers)179 protected boolean _asTimestamp(SerializerProvider serializers) 180 { 181 if (_useTimestamp != null) { 182 return _useTimestamp.booleanValue(); 183 } 184 if (_customFormat == null) { 185 if (serializers != null) { 186 return serializers.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); 187 } 188 // 12-Jun-2014, tatu: Is it legal not to have provider? Was NPE:ing earlier so leave a check 189 throw new IllegalArgumentException("Null SerializerProvider passed for "+handledType().getName()); 190 } 191 return false; 192 } 193 _acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint, boolean asNumber)194 protected void _acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint, 195 boolean asNumber) throws JsonMappingException 196 { 197 if (asNumber) { 198 visitIntFormat(visitor, typeHint, 199 JsonParser.NumberType.LONG, JsonValueFormat.UTC_MILLISEC); 200 } else { 201 visitStringFormat(visitor, typeHint, JsonValueFormat.DATE_TIME); 202 } 203 } 204 205 /** 206 * @since 2.9 207 */ _serializeAsString(Date value, JsonGenerator g, SerializerProvider provider)208 protected void _serializeAsString(Date value, JsonGenerator g, SerializerProvider provider) throws IOException 209 { 210 if (_customFormat == null) { 211 provider.defaultSerializeDateValue(value, g); 212 return; 213 } 214 215 // 19-Jul-2017, tatu: Here we will try a simple but (hopefully) effective mechanism for 216 // reusing formatter instance. This is our second attempt, after initially trying simple 217 // synchronization (which turned out to be bottleneck for some users in production...). 218 // While `ThreadLocal` could alternatively be used, it is likely that it would lead to 219 // higher memory footprint, but without much upside -- if we can not reuse, we'll just 220 // clone(), which has some overhead but not drastic one. 221 222 DateFormat f = _reusedCustomFormat.getAndSet(null); 223 if (f == null) { 224 f = (DateFormat) _customFormat.clone(); 225 } 226 g.writeString(f.format(value)); 227 _reusedCustomFormat.compareAndSet(null, f); 228 } 229 } 230