001/*
002 * Copyright (c) 2016-2018 Chris K Wensel. All Rights Reserved.
003 *
004 * Project and contact information: http://www.cascading.org/
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.util.Collection;
026import java.util.List;
027import java.util.Map;
028
029import cascading.CascadingException;
030import cascading.nested.core.NestedCoercibleType;
031import cascading.tuple.coerce.Coercions;
032import cascading.tuple.type.CoercibleType;
033import cascading.tuple.type.SerializableType;
034import cascading.util.Util;
035import com.fasterxml.jackson.core.JsonParseException;
036import com.fasterxml.jackson.core.JsonProcessingException;
037import com.fasterxml.jackson.databind.DeserializationFeature;
038import com.fasterxml.jackson.databind.JsonNode;
039import com.fasterxml.jackson.databind.ObjectMapper;
040import com.fasterxml.jackson.databind.node.ArrayNode;
041import com.fasterxml.jackson.databind.node.JsonNodeFactory;
042import com.fasterxml.jackson.databind.node.JsonNodeType;
043import heretical.pointer.path.NestedPointerCompiler;
044import heretical.pointer.path.json.JSONNestedPointerCompiler;
045
046/**
047 * Class JSONCoercibleType is a {@link NestedCoercibleType} that provides support
048 * for JSON object types.
049 * <p>
050 * Supported values will be maintained as a {@link JsonNode} canonical type within the {@link cascading.tuple.Tuple}.
051 * <p>
052 * Note that {@link #canonical(Object)} will always attempt to parse a String value to a new JsonNode.
053 * If the parse fails, it will return a {@link com.fasterxml.jackson.databind.node.TextNode} instance wrapping the
054 * String value.
055 * <p>
056 * See the {@link #node(Object)}.
057 */
058public class JSONCoercibleType implements NestedCoercibleType<JsonNode, ArrayNode>, SerializableType
059  {
060  public static final JSONCoercibleType TYPE = new JSONCoercibleType();
061
062  private ObjectMapper mapper = new ObjectMapper();
063
064  private JSONCoercibleType()
065    {
066    // prevents json object from being created with duplicate names at the same level
067    mapper.setConfig( mapper.getDeserializationConfig()
068      .with( DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY ) );
069    }
070
071  @Override
072  public Class<JsonNode> getCanonicalType()
073    {
074    return JsonNode.class;
075    }
076
077  @Override
078  public JsonNode canonical( Object value )
079    {
080    if( value == null )
081      return null;
082
083    Class from = value.getClass();
084
085    if( JsonNode.class.isAssignableFrom( from ) )
086      return (JsonNode) value;
087
088    if( from == String.class )
089      return nodeOrParse( (String) value );
090
091    if( from == Integer.class || from == Integer.TYPE )
092      return JsonNodeFactory.instance.numberNode( (Integer) value );
093
094    if( from == Long.class || from == Long.TYPE )
095      return JsonNodeFactory.instance.numberNode( (Long) value );
096
097    if( from == Float.class || from == Float.TYPE )
098      return JsonNodeFactory.instance.numberNode( (Float) value );
099
100    if( from == Double.class || from == Double.TYPE )
101      return JsonNodeFactory.instance.numberNode( (Double) value );
102
103    if( from == Boolean.class || from == Boolean.TYPE )
104      return JsonNodeFactory.instance.booleanNode( (Boolean) value );
105
106    if( Collection.class.isAssignableFrom( from ) || Map.class.isAssignableFrom( from ) )
107      return mapper.valueToTree( value );
108
109    throw new CascadingException( "unknown type coercion requested from: " + Util.getTypeName( from ) );
110    }
111
112  @Override
113  public <Coerce> Coerce coerce( Object value, Type to )
114    {
115    if( to == null || to.getClass() == JSONCoercibleType.class )
116      return (Coerce) value;
117
118    if( value == null )
119      return null;
120
121    Class from = value.getClass();
122
123    if( !JsonNode.class.isAssignableFrom( from ) )
124      throw new IllegalStateException( "was not normalized, got: " + from.getName() );
125
126    JsonNode node = (JsonNode) value;
127
128    if( node.isMissingNode() )
129      return null;
130
131    JsonNodeType nodeType = node.getNodeType();
132
133    if( nodeType == JsonNodeType.NULL )
134      return null;
135
136    if( to == String.class )
137      return nodeType == JsonNodeType.STRING ? (Coerce) node.textValue() : (Coerce) textOrWrite( node );
138
139    if( to == Integer.class || to == Integer.TYPE )
140      return nodeType == JsonNodeType.NUMBER ? (Coerce) Integer.valueOf( node.intValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
141
142    if( to == Long.class || to == Long.TYPE )
143      return nodeType == JsonNodeType.NUMBER ? (Coerce) Long.valueOf( node.longValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
144
145    if( to == Float.class || to == Float.TYPE )
146      return nodeType == JsonNodeType.NUMBER ? (Coerce) Float.valueOf( node.floatValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
147
148    if( to == Double.class || to == Double.TYPE )
149      return nodeType == JsonNodeType.NUMBER ? (Coerce) Double.valueOf( node.doubleValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
150
151    if( to == Boolean.class || to == Boolean.TYPE )
152      return nodeType == JsonNodeType.BOOLEAN ? (Coerce) Boolean.valueOf( node.booleanValue() ) : (Coerce) Coercions.coerce( textOrWrite( node ), to );
153
154    if( Map.class.isAssignableFrom( (Class<?>) to ) )
155      return (Coerce) convert( value, (Class) to );
156
157    if( List.class.isAssignableFrom( (Class<?>) to ) )
158      return (Coerce) convert( value, (Class) to );
159
160    throw new CascadingException( "unknown type coercion requested, from: " + Util.getTypeName( from ) + " to: " + Util.getTypeName( to ) );
161    }
162
163  private Object convert( Object value, Class to )
164    {
165    return mapper.convertValue( value, to );
166    }
167
168  private String textOrWrite( JsonNode value )
169    {
170    if( value != null && value.isTextual() )
171      return value.textValue();
172
173    try
174      {
175      return write( value );
176      }
177    catch( JsonProcessingException exception )
178      {
179      throw new CascadingException( "unable to write value as json", exception );
180      }
181    }
182
183  private String write( JsonNode value ) throws JsonProcessingException
184    {
185    return mapper.writeValueAsString( value );
186    }
187
188  private JsonNode nodeOrParse( String value )
189    {
190    try
191      {
192      return parse( value ); // presume this is a JSON string
193      }
194    catch( JsonParseException exception )
195      {
196      return JsonNodeFactory.instance.textNode( value );
197      }
198    catch( IOException exception )
199      {
200      throw new CascadingException( "unable to read json", exception );
201      }
202    }
203
204  private JsonNode parse( String value ) throws IOException
205    {
206    return mapper.readTree( value );
207    }
208
209  @Override
210  public NestedPointerCompiler<JsonNode, ArrayNode> getNestedPointerCompiler()
211    {
212    return JSONNestedPointerCompiler.COMPILER;
213    }
214
215  @Override
216  public JsonNode deepCopy( JsonNode jsonNode )
217    {
218    if( jsonNode == null )
219      return null;
220
221    return jsonNode.deepCopy();
222    }
223
224  @Override
225  public JsonNode newRoot()
226    {
227    return JsonNodeFactory.instance.objectNode();
228    }
229
230  @Override
231  public Class getSerializer( Class base )
232    {
233    // required to defer classloading
234    if( base == org.apache.hadoop.io.serializer.Serialization.class )
235      return cascading.nested.json.hadoop2.JSONHadoopSerialization.class;
236
237    return null;
238    }
239
240  @Override
241  public String toString()
242    {
243    return getClass().getName();
244    }
245
246  @Override
247  public int hashCode()
248    {
249    return getCanonicalType().hashCode();
250    }
251
252  @Override
253  public boolean equals( Object object )
254    {
255    if( this == object )
256      return true;
257
258    if( !( object instanceof CoercibleType ) )
259      return false;
260
261    return getCanonicalType().equals( ( (CoercibleType) object ).getCanonicalType() );
262    }
263  }