001/*
002 * Copyright (c) 2007-2022 The Cascading Authors. All Rights Reserved.
003 *
004 * Project and contact information: https://cascading.wensel.net/
005 *
006 * This file is part of the Cascading project.
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *     http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020
021package cascading.nested.json;
022
023import java.io.IOException;
024import java.lang.reflect.Type;
025import java.time.temporal.Temporal;
026import java.time.temporal.TemporalAmount;
027import java.util.Collection;
028import java.util.List;
029import java.util.Map;
030import java.util.function.Function;
031
032import cascading.CascadingException;
033import cascading.nested.core.NestedCoercibleType;
034import cascading.tuple.coerce.Coercions;
035import cascading.tuple.type.CoercibleType;
036import cascading.tuple.type.CoercionFrom;
037import cascading.tuple.type.SerializableType;
038import cascading.tuple.type.ToCanonical;
039import cascading.util.Util;
040import com.fasterxml.jackson.core.JsonParseException;
041import com.fasterxml.jackson.core.JsonProcessingException;
042import com.fasterxml.jackson.databind.DeserializationFeature;
043import com.fasterxml.jackson.databind.JsonNode;
044import com.fasterxml.jackson.databind.Module;
045import com.fasterxml.jackson.databind.ObjectMapper;
046import com.fasterxml.jackson.databind.node.ArrayNode;
047import com.fasterxml.jackson.databind.node.JsonNodeFactory;
048import com.fasterxml.jackson.databind.node.JsonNodeType;
049import heretical.pointer.path.NestedPointerCompiler;
050import heretical.pointer.path.json.JSONNestedPointerCompiler;
051
052/**
053 * Class JSONCoercibleType is a {@link NestedCoercibleType} that provides support for JSON object types.
054 * <p>
055 * Supported values will be maintained as a {@link JsonNode} canonical type within the {@link cascading.tuple.Tuple}.
056 * <p>
057 * Note that {@link #canonical(Object)} will always attempt to parse a String value to a new JsonNode.
058 * If the parse fails, it will return a {@link com.fasterxml.jackson.databind.node.TextNode} instance wrapping the
059 * String value.
060 * <p>
061 * Note the default instance (@link {@link #TYPE}), sets the {@link DeserializationFeature#FAIL_ON_READING_DUP_TREE_KEY}
062 * Jackson property.
063 * <p>
064 * See the {@link #node(Object)}.
065 */
066public class JSONCoercibleType implements NestedCoercibleType<JsonNode, ArrayNode>, SerializableType
067  {
068  public static final JSONCoercibleType TYPE = new JSONCoercibleType();
069
070  private ObjectMapper mapper = new ObjectMapper();
071
072  private JSONCoercibleType()
073    {
074    // prevents json object from being created with duplicate names at the same level
075    mapper.setConfig( mapper.getDeserializationConfig()
076      .with( DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY ) );
077    }
078
079  /**
080   * Instantiates a new Json coercible type with the given {@link ObjectMapper} instance.
081   * <p>
082   * Use this constructor in order to leverage {@link Module} instances.
083   * <p>
084   * Note the default instance (@link {@link #TYPE}), sets the {@link DeserializationFeature#FAIL_ON_READING_DUP_TREE_KEY}
085   * Jackson property.
086   *
087   * @param mapper the mapper
088   */
089  public JSONCoercibleType( ObjectMapper mapper )
090    {
091    this.mapper = mapper;
092    }
093
094  @Override
095  public Class<JsonNode> getCanonicalType()
096    {
097    return JsonNode.class;
098    }
099
100  @Override
101  public <T> ToCanonical<T, JsonNode> from( Type from )
102    {
103    if( from.getClass() == JSONCoercibleType.class )
104      return ( v ) -> (JsonNode) v;
105
106    if( from instanceof Class && JsonNode.class.isAssignableFrom( (Class<?>) from ) )
107      return ( v ) -> (JsonNode) v;
108
109    if( from == String.class )
110      return ( v ) -> v == null ? null : nodeOrParse( (String) v );
111
112    if( from == Short.class || from == Short.TYPE )
113      return ( v ) -> v == null ? null : JsonNodeFactory.instance.numberNode( (Short) v );
114
115    if( from == Integer.class || from == Integer.TYPE )
116      return ( v ) -> v == null ? null : JsonNodeFactory.instance.numberNode( (Integer) v );
117
118    if( from == Long.class || from == Long.TYPE )
119      return ( v ) -> v == null ? null : JsonNodeFactory.instance.numberNode( (Long) v );
120
121    if( from == Float.class || from == Float.TYPE )
122      return ( v ) -> v == null ? null : JsonNodeFactory.instance.numberNode( (Float) v );
123
124    if( from == Double.class || from == Double.TYPE )
125      return ( v ) -> v == null ? null : JsonNodeFactory.instance.numberNode( (Double) v );
126
127    if( from == Boolean.class || from == Boolean.TYPE )
128      return ( v ) -> v == null ? null : JsonNodeFactory.instance.booleanNode( (Boolean) v );
129
130    if( from instanceof Class && ( Collection.class.isAssignableFrom( (Class<?>) from ) || Map.class.isAssignableFrom( (Class<?>) from ) ) )
131      return ( v ) -> v == null ? null : mapper.valueToTree( v );
132
133    return ( v ) -> v == null ? null : JsonNodeFactory.instance.pojoNode( v );
134    }
135
136  @Override
137  public JsonNode canonical( Object value )
138    {
139    if( value == null )
140      return null;
141
142    Class from = value.getClass();
143
144    if( JsonNode.class.isAssignableFrom( from ) )
145      return (JsonNode) value;
146
147    if( from == String.class )
148      return nodeOrParse( (String) value );
149
150    if( from == Short.class || from == Short.TYPE )
151      return JsonNodeFactory.instance.numberNode( (Short) value );
152
153    if( from == Integer.class || from == Integer.TYPE )
154      return JsonNodeFactory.instance.numberNode( (Integer) value );
155
156    if( from == Long.class || from == Long.TYPE )
157      return JsonNodeFactory.instance.numberNode( (Long) value );
158
159    if( from == Float.class || from == Float.TYPE )
160      return JsonNodeFactory.instance.numberNode( (Float) value );
161
162    if( from == Double.class || from == Double.TYPE )
163      return JsonNodeFactory.instance.numberNode( (Double) value );
164
165    if( from == Boolean.class || from == Boolean.TYPE )
166      return JsonNodeFactory.instance.booleanNode( (Boolean) value );
167
168    if( Collection.class.isAssignableFrom( from ) || Map.class.isAssignableFrom( from ) )
169      return mapper.valueToTree( value );
170
171    return JsonNodeFactory.instance.pojoNode( value );
172    }
173
174  protected <T> T ifNull( JsonNode node, Function<JsonNode, T> function )
175    {
176    if( node == null || node.getNodeType() == JsonNodeType.NULL || node.getNodeType() == JsonNodeType.MISSING )
177      return null;
178
179    return function.apply( node );
180    }
181
182  @Override
183  public <T> CoercionFrom<JsonNode, T> to( Type to )
184    {
185    if( to == null || to.getClass() == JSONCoercibleType.class )
186      return t -> (T) t;
187
188    Class<?> actualTo;
189
190    if( to instanceof CoercibleType )
191      actualTo = ( (CoercibleType<?>) to ).getCanonicalType();
192    else
193      actualTo = (Class<?>) to;
194
195    if( JsonNode.class.isAssignableFrom( actualTo ) )
196      return t -> (T) t;
197
198    if( actualTo == String.class )
199      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.STRING ? (T) n.textValue() : (T) textOrWrite( n ) );
200
201    if( actualTo == Short.class || actualTo == Short.TYPE )
202      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.NUMBER ? (T) Short.valueOf( n.shortValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
203
204    if( actualTo == Integer.class || actualTo == Integer.TYPE )
205      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.NUMBER ? (T) Integer.valueOf( n.intValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
206
207    if( actualTo == Long.class || actualTo == Long.TYPE )
208      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.NUMBER ? (T) Long.valueOf( n.longValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
209
210    if( actualTo == Float.class || actualTo == Float.TYPE )
211      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.NUMBER ? (T) Float.valueOf( n.floatValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
212
213    if( actualTo == Double.class || actualTo == Double.TYPE )
214      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.NUMBER ? (T) Double.valueOf( n.doubleValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
215
216    if( actualTo == Boolean.class || actualTo == Boolean.TYPE )
217      return node -> ifNull( node, n -> n.getNodeType() == JsonNodeType.BOOLEAN ? (T) Boolean.valueOf( n.booleanValue() ) : Coercions.coerce( textOrWrite( n ), to ) );
218
219    if( Map.class.isAssignableFrom( actualTo ) )
220      return node -> ifNull( node, n -> (T) convertTree( n, actualTo ) );
221
222    if( List.class.isAssignableFrom( actualTo ) )
223      return node -> ifNull( node, n -> (T) convertTree( n, actualTo ) );
224
225    if( Temporal.class.isAssignableFrom( actualTo ) )
226      return node -> ifNull( node, n -> (T) convertTree( n, actualTo ) );
227
228    if( TemporalAmount.class.isAssignableFrom( actualTo ) )
229      return node -> ifNull( node, n -> (T) convertTree( n, actualTo ) );
230
231    if( to instanceof CoercibleType )
232      return node -> ifNull( node, n -> (T) ( (CoercibleType<?>) to ).canonical( textOrWrite( n ) ) );
233
234    throw new CascadingException( "unknown type coercion requested, from: " + Util.getTypeName( JsonNode.class ) + " to: " + Util.getTypeName( to ) );
235    }
236
237  @Override
238  public <Coerce> Coerce coerce( Object value, Type to )
239    {
240    if( to == null || to.getClass() == JSONCoercibleType.class )
241      return (Coerce) value;
242
243    if( value == null )
244      return null;
245
246    Class<?> from = value.getClass();
247
248    if( !JsonNode.class.isAssignableFrom( from ) )
249      throw new IllegalStateException( "was not normalized, got: " + from.getName() );
250
251    JsonNode node = (JsonNode) value;
252
253    if( node.isMissingNode() )
254      return null;
255
256    JsonNodeType nodeType = node.getNodeType();
257
258    if( nodeType == JsonNodeType.NULL )
259      return null;
260
261    Class<?> actualTo;
262
263    if( to instanceof CoercibleType )
264      actualTo = ( (CoercibleType<?>) to ).getCanonicalType();
265    else
266      actualTo = (Class<?>) to;
267
268    // support sub-classes of JsonNode
269    if( JsonNode.class.isAssignableFrom( actualTo ) )
270      return (Coerce) node;
271
272    if( actualTo == String.class )
273      return nodeType == JsonNodeType.STRING ? (Coerce) node.textValue() : (Coerce) textOrWrite( node );
274
275    if( actualTo == Short.class || actualTo == Short.TYPE )
276      return nodeType == JsonNodeType.NUMBER ? (Coerce) Short.valueOf( node.shortValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
277
278    if( actualTo == Integer.class || actualTo == Integer.TYPE )
279      return nodeType == JsonNodeType.NUMBER ? (Coerce) Integer.valueOf( node.intValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
280
281    if( actualTo == Long.class || actualTo == Long.TYPE )
282      return nodeType == JsonNodeType.NUMBER ? (Coerce) Long.valueOf( node.longValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
283
284    if( actualTo == Float.class || actualTo == Float.TYPE )
285      return nodeType == JsonNodeType.NUMBER ? (Coerce) Float.valueOf( node.floatValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
286
287    if( actualTo == Double.class || actualTo == Double.TYPE )
288      return nodeType == JsonNodeType.NUMBER ? (Coerce) Double.valueOf( node.doubleValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
289
290    if( actualTo == Boolean.class || actualTo == Boolean.TYPE )
291      return nodeType == JsonNodeType.BOOLEAN ? (Coerce) Boolean.valueOf( node.booleanValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
292
293    if( Map.class.isAssignableFrom( actualTo ) )
294      return (Coerce) convertTree( node, actualTo );
295
296    if( List.class.isAssignableFrom( actualTo ) )
297      return (Coerce) convertTree( node, actualTo );
298
299    if( Temporal.class.isAssignableFrom( actualTo ) )
300      return (Coerce) convertTree( node, actualTo );
301
302    if( TemporalAmount.class.isAssignableFrom( actualTo ) )
303      return (Coerce) convertTree( node, actualTo );
304
305    if( to instanceof CoercibleType )
306      return (Coerce) ( (CoercibleType<?>) to ).canonical( textOrWrite( node ) );
307
308    throw new CascadingException( "unknown type coercion requested, from: " + Util.getTypeName( from ) + " to: " + Util.getTypeName( to ) );
309    }
310
311  private Object convertTree( JsonNode value, Class<?> to )
312    {
313    try
314      {
315      return mapper.treeToValue( value, to );
316      }
317    catch( JsonProcessingException exception )
318      {
319      throw new CascadingException( "unable to coerce json node into " + to.getName(), exception );
320      }
321    }
322
323  private String textOrWrite( JsonNode value )
324    {
325    if( value != null && value.isTextual() )
326      return value.textValue();
327
328    try
329      {
330      return write( value );
331      }
332    catch( JsonProcessingException exception )
333      {
334      throw new CascadingException( "unable to write value as json", exception );
335      }
336    }
337
338  private String write( JsonNode value ) throws JsonProcessingException
339    {
340    return mapper.writeValueAsString( value );
341    }
342
343  private JsonNode nodeOrParse( String value )
344    {
345    try
346      {
347      return parse( value ); // presume this is a JSON string
348      }
349    catch( JsonParseException exception )
350      {
351      return JsonNodeFactory.instance.textNode( value );
352      }
353    catch( IOException exception )
354      {
355      throw new CascadingException( "unable to read json", exception );
356      }
357    }
358
359  private JsonNode parse( String value ) throws IOException
360    {
361    return mapper.readTree( value );
362    }
363
364  @Override
365  public NestedPointerCompiler<JsonNode, ArrayNode> getNestedPointerCompiler()
366    {
367    return JSONNestedPointerCompiler.COMPILER;
368    }
369
370  @Override
371  public JsonNode deepCopy( JsonNode jsonNode )
372    {
373    if( jsonNode == null )
374      return null;
375
376    return jsonNode.deepCopy();
377    }
378
379  @Override
380  public JsonNode newRoot()
381    {
382    return JsonNodeFactory.instance.objectNode();
383    }
384
385  @Override
386  public Class getSerializer( Class base )
387    {
388    // required to defer classloading
389    if( base == org.apache.hadoop.io.serializer.Serialization.class )
390      return cascading.nested.json.hadoop2.JSONHadoopSerialization.class;
391
392    return null;
393    }
394
395  @Override
396  public String toString()
397    {
398    return getClass().getName();
399    }
400
401  @Override
402  public int hashCode()
403    {
404    return getCanonicalType().hashCode();
405    }
406
407  @Override
408  public boolean equals( Object object )
409    {
410    if( this == object )
411      return true;
412
413    if( !( object instanceof CoercibleType ) )
414      return false;
415
416    return getCanonicalType().equals( ( (CoercibleType) object ).getCanonicalType() );
417    }
418  }