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 }