/************************************************************************* * (c) Copyright 2016 Hewlett Packard Enterprise Development Company LP * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. ************************************************************************/ package com.eucalyptus.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.StringReader; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.node.ObjectNode; import edu.ucsb.eucalyptus.msgs.BaseMessage; import groovy.lang.GroovyObject; import groovy.lang.MetaClass; import javaslang.Function1; import javaslang.Predicates; import javaslang.collection.Stream; import javaslang.control.Option; /** * Utility class for JSON processing with Jackson. */ public class Json { private static final ObjectMapper mapper = mapper( ); private static final ObjectReader reader = mapper.reader( ); private static final ObjectWriter writer = mapper.writer( ); private static final Option<Stream<String>> optionStreamEmpty = Option.of( Stream.empty( ) ); private static final Option<Stream<JsonNode>> optionStreamObjEmpty = Option.of( Stream.empty( ) ); private static final Function1<String,Supplier<IOException>> missingExceptionSupplierFunc = fieldName -> () -> new IOException( "Missing required value " + fieldName ); public enum JsonOption { IgnoreGroovy { @Override ObjectMapper config( final ObjectMapper objectMapper ) { objectMapper.addMixIn( GroovyObject.class, GroovyMixin.class ); return objectMapper; } }, IgnoreBaseMessage { @Override ObjectMapper config( final ObjectMapper objectMapper ) { objectMapper.addMixIn( BaseMessage.class, BaseMessageMixin.class ); return objectMapper; } }, ; abstract ObjectMapper config( final ObjectMapper mapper ); static ObjectMapper config( final Iterable<JsonOption> options, final ObjectMapper objectMapper ) { ObjectMapper configured = objectMapper; for ( final JsonOption option : options ) { configured = option.config( configured ); } return configured; } } public static JsonNode parse( final InputStream jsonStream ) throws IOException { if ( jsonStream == null ) throw new IOException( "Null" ); final JsonParser parser = reader.getFactory( ).createParser( jsonStream ); return parse( parser ); } public static JsonNode parse( final String jsonText ) throws IOException { if ( jsonText == null ) throw new IOException( "Null" ); final JsonParser parser = reader.getFactory( ).createParser( new StringReader( jsonText ) { @Override public String toString() { return "json"; } // overridden for better source in error message } ); return parse( parser ); } public static JsonNode parse( final JsonParser parser ) throws IOException { if ( parser == null ) throw new IOException( "Null" ); final JsonNode node = reader.readTree( parser ); boolean trailingContent; try { trailingContent = parser.nextToken( ) != null; } catch ( IOException e ) { trailingContent = true; } if ( trailingContent ) { throw new IOException( "Unexpected trailing content at " + parser.getCurrentLocation( ) ); } return node; } public static ObjectNode parseObject( final String jsonText ) throws IOException { final JsonNode node = parse( jsonText ); if ( !node.isObject( ) ) { throw new IOException( "Invalid object" ); } return (ObjectNode) node; } public static void writeObject( final OutputStream out, final Object object ) throws IOException { writer.writeValue( out, object ); } public static String writeObjectAsString( final Object object ) throws IOException { return writer.writeValueAsString( object ); } /** * Get a mapper configured to ignore groovy object properties. */ public static ObjectMapper mapper( ) { return JsonOption.IgnoreGroovy.config( new ObjectMapper( ) ); } /** * Get a mapper configured with the given options. */ public static ObjectMapper mapper( final Iterable<JsonOption> options ) { return JsonOption.config( options, new ObjectMapper( ) ); } public static boolean isText( final JsonNode parent, final String fieldName ) throws IOException { final JsonNode node = parent.get( fieldName ); return node != null && node.isTextual( ); } public static String text( final JsonNode parent, final String fieldName ) throws IOException { return textOption( parent, fieldName ).getOrElseThrow( ifMissing( fieldName ) ); } public static Option<String> textOption( final JsonNode parent, final String fieldName ) throws IOException { return tOption( parent, fieldName, JsonNode::isTextual, JsonNode::asText ); } public static Integer integer( final JsonNode parent, final String fieldName ) throws IOException { return integerOption( parent, fieldName ).getOrElseThrow( ifMissing( fieldName ) ); } public static Option<Integer> integerOption( final JsonNode parent, final String fieldName ) throws IOException { return tOption( parent, fieldName, Predicates.allOf( JsonNode::isIntegralNumber, JsonNode::canConvertToInt ), JsonNode::asInt ); } public static Long longInt( final JsonNode parent, final String fieldName ) throws IOException { return longIntOption( parent, fieldName ).getOrElseThrow( ifMissing( fieldName ) ); } public static Option<Long> longIntOption( final JsonNode parent, final String fieldName ) throws IOException { return tOption( parent, fieldName, Predicates.allOf( JsonNode::isIntegralNumber, JsonNode::canConvertToLong ), JsonNode::asLong ); } public static Option<List<JsonNode>> objectListOption( final JsonNode parent, final String fieldName ) throws IOException { return tListOption( parent, fieldName, optionStreamObjEmpty, ( streamOption, item ) -> streamOption.flatMap( stream -> item.isObject( ) ? Option.of( stream.append( item ) ) : Option.<Stream<JsonNode>>none( ) ) ); } public static List<JsonNode> objectList( final JsonNode parent, final String fieldName ) throws IOException { return objectListOption( parent, fieldName ).getOrElseThrow( ifMissing( fieldName ) ); } public static Option<List<String>> textListOption( final JsonNode parent, final String fieldName ) throws IOException { return tListOption( parent, fieldName, optionStreamEmpty, ( streamOption, item ) -> streamOption.flatMap( stream -> item.isTextual( ) ? Option.of( stream.append( item.asText( ) ) ) : Option.<Stream<String>>none( ) ) ); } public static List<String> textList( final JsonNode parent, final String fieldName ) throws IOException { return textListOption( parent, fieldName ).getOrElseThrow( ifMissing( fieldName ) ); } private static <T> Option<T> tOption( final JsonNode parent, final String fieldName, final Predicate<? super JsonNode> test, final Function<? super JsonNode,? extends T> mapper ) throws IOException { final JsonNode node = parent.get( fieldName ); if ( node == null ) { return Option.none( ); } else if ( test.test( node ) ) { return Option.of( mapper.apply( node ) ); } else { throw new IOException( "Invalid content for " + fieldName ); } } private static <T> Option<List<T>> tListOption( final JsonNode parent, final String fieldName, final Option<Stream<T>> emptyStream, final BiFunction<Option<Stream<T>>,JsonNode,Option<Stream<T>>> reducer ) throws IOException { final JsonNode node = parent.get( fieldName ); if ( node == null ) { return Option.none( ); } else if ( node.isArray( ) ) { return Option.of( CollectionUtils.reduce( node, emptyStream, reducer ) .getOrElseThrow( ( ) -> new IOException( "Invalid array content for " + fieldName ) ) .toJavaList( ) ); } else { throw new IOException( "Invalid content for " + fieldName ); } } private static Supplier<IOException> ifMissing( final String fieldName ) { return missingExceptionSupplierFunc.apply( fieldName ); } private interface GroovyMixin { @JsonIgnore void setMetaClass( MetaClass var1); @JsonIgnore MetaClass getMetaClass( ); } @JsonIgnoreProperties( { "correlationId", "effectiveUserId", "reply", "statusMessage", "userId", "_disabledServices", "_notreadyServices", "_stoppedServices", "_epoch", "_services", "_return", "callerContext" } ) private interface BaseMessageMixin { } }