All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bazaarvoice.jolt.utils.JoltUtils Maven / Gradle / Ivy

There is a newer version: 0.1.8
Show newest version
/*
 * Copyright 2013 Bazaarvoice, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bazaarvoice.jolt.utils;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Handy utilities that do NOT depend on JsonUtil / Jackson live here
 */
public class JoltUtils {

    /**
     * Removes a key recursively from anywhere in a JSON document.
     * NOTE: mutates its input.
     *
     * @param json        the Jackson Object version of the JSON document
     *                    (contents changed by this call)
     * @param keyToRemove the key to remove from the document
     */
    public static void removeRecursive( Object json, String keyToRemove ) {
        if ( ( json == null ) || ( keyToRemove == null ) ) {
            return;
        }
        if ( json instanceof Map ) {
            Map jsonMap = cast(json);

            // If this level of the tree has the key we are looking for, remove it
            // Do the lookup instead of just the remove to avoid un-necessarily
            //  dying on ImmutableMaps.
            if ( jsonMap.containsKey( keyToRemove ) ) {
                jsonMap.remove( keyToRemove );
            }

            // regardless, recurse down the tree
            for ( Object value : jsonMap.values() ) {
                removeRecursive( value, keyToRemove );
            }
        }
        if ( json instanceof List ) {
            for ( Object value : (List) json ) {
                removeRecursive( value, keyToRemove );
            }
        }
    }

    /**
     * Navigate inside a json object in quick and dirty way.
     *
     * @param source the source json object
     * @param paths the paths array to travel
     * @return the object of Type  at final destination
     * @throws NullPointerException if the source is null
     * @throws UnsupportedOperationException if the source is not Map or List
     */
    public static  T navigate(final Object source, final Object... paths) {
        Object destination = source;
        for (Object path : paths) {
            if(path == null || destination == null) {
                throw new NullPointerException("source or path is null");
            }
            if(destination instanceof Map) {
                destination = ((Map) destination).get(path);
            }
            else if(path instanceof Integer && destination instanceof List) {
                destination = ((List) destination).get((Integer)path);
            }
            else {
                throw new UnsupportedOperationException("Navigation supports only Map and List source types and non-null String and Integer path types");
            }
        }
        return cast(destination);
    }

    /**
     * Navigate inside a json object in quick and "dirtier" way, i.e. returns a default value NPE
     * or out-of-index errors encountered
     *
     * @param source the source
     * @param paths the paths array
     * @return the object of Type  at final destination or defaultValue if non existent
     * @throws UnsupportedOperationException the unsupported operation exception
     */
    public static  T navigateSafe(final T defaultValue, final Object source, final Object... paths) {
        Object destination = source;
        for (Object path : paths) {
            if(path == null || destination == null) {
                return defaultValue;
            }
            if(destination instanceof Map) {
                Map destinationMap = (Map) destination;
                if(!destinationMap.containsKey(path)) {
                    return defaultValue;
                }
                else {
                    destination = destinationMap.get(path);
                }
            }
            else if(path instanceof Integer && destination instanceof List) {
                List destinationList = (List) destination;
                if ( (Integer) path >= destinationList.size() ) {
                    return defaultValue;
                }
                else {
                    destination = destinationList.get((Integer)path);
                }
            }
            else {
                throw new UnsupportedOperationException("Navigation supports only Map and List source types and non-null String and Integer path types");
            }
        }
        return cast(destination);
    }

    /**
     * Deprecated: use isVacantJson() instead
     */
    @Deprecated
    public static boolean isEmptyJson(final Object obj) {
        return isVacantJson(obj);
    }
    /**
     * Vacant implies there are empty placeholders, i.e. a vacant hotel
     * Given a json document, checks if it has any "leaf" values, can handle deep nesting of lists and maps
     *
     * i.e. { "a": [ "x": {}, "y": [] ], "b": { "p": [], "q": {} }} ==> is empty
     *
     * @param obj source
     * @return true if its an empty json, can have deep nesting, false otherwise
     */
    public static boolean isVacantJson(final Object obj) {
        Collection values = null;
        if(obj instanceof Collection) {
            if(((Collection) obj).size() == 0) {
                return true;
            }
            values = (Collection) obj;
        }
        if(obj instanceof Map) {
            if(((Map) obj).size() == 0) {
                return true;
            }
            values = ((Map) obj).values();
        }
        int processedEmpty = 0;
        if(values != null) {
            for (Object value: values) {
                if(!isVacantJson(value)) {
                    return false;
                }
                processedEmpty++;
            }
            if(processedEmpty == values.size()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Given a json document checks if its jst blank doc, i.e. [] or {}
     *
     * @param obj source
     * @return true if the json doc is [] or {}
     */
    public static boolean isBlankJson(final Object obj) {
        if (obj == null) {
            return true;
        }
        if(obj instanceof Collection) {
            return (((Collection) obj).size() == 0);
        }
        if(obj instanceof Map) {
            return (((Map) obj).size() == 0);
        }
        throw new UnsupportedOperationException("map or list is supported, got ${obj?obj.getClass():null}");
    }

    /**
     * Given a json document, finds out absolute path to every leaf element
     *
     * i.e. { "a": [ "x": { "y": "alpha" }], "b": { "p": [ "beta", "gamma" ], "q": {} }} will yield
     *
     * 1) "a",0,"x","y" -> to "alpha"
     * 2) "b","p", 0 -> to "beta"
     * 3) "b", "p", 1 -> to "gamma"
     * 4) "b","q" -> to {} (empty Map)
     *
     * @param source json
     * @return list of Object[] representing path to every leaf element
     */
    public static List listKeyChains(final Object source) {

        List keyChainList = new LinkedList<>();

        if(source instanceof Map) {
            Map sourceMap = (Map) source;
            for (Object key: sourceMap.keySet()) {
                keyChainList.addAll(listKeyChains(key, sourceMap.get(key)));
            }
        }
        else if(source instanceof List) {
            List sourceList = (List) source;
            for(int i=0; i listKeyChains(final Object key, final Object value) {
        List keyChainList = new LinkedList<>();
        List childKeyChainList = listKeyChains(value);
        if(childKeyChainList.size() > 0) {
            for(Object[] childKeyChain: childKeyChainList) {
                Object[] keyChain = new Object[childKeyChain.length + 1];
                keyChain[0] = key;
                System.arraycopy(childKeyChain, 0, keyChain, 1, childKeyChain.length);
                keyChainList.add(keyChain);
            }
        }
        else {
            keyChainList.add(new Object[] {key});
        }
        return keyChainList;
    }

    /**
     * Converts a standard json path to human readable SimpleTraversr compatible path
     *
     * @param paths the path array of objects
     * @return string representation of the path, human readable and SimpleTraversr friendly
     */
    public static String toSimpleTraversrPath(Object[] paths) {
        StringBuilder pathBuilder = new StringBuilder();
        for(int i=0; i T cast(Object object) {
        return (T) (object);
    }

    /**
     * Type cast to array E[]
     *
     * @param object the input object to cast
     * @return casted array of type E[]
     */
    @SuppressWarnings("unchecked")
    public static  E[] cast(Object[] object) {
        return (E[])(object);
    }

    /**
     * Given a 'fluffy' json document, it recursively removes all null elements
     * to compact the json document
     *
     * Warning: mutates the doc, destroys array order
     *
     * @param source
     * @return mutated source where all null elements are nuked
     */
    @SuppressWarnings("unchecked")
    public static Object compactJson(Object source) {
        if (source == null) return null;

        if (source instanceof List) {
            for (Object item : (List) source) {
                if (item instanceof List) {
                    compactJson(item);
                }
                else if (item instanceof Map) {
                    compactJson(item);
                }
            }
            ((List) source).removeAll(Collections.singleton(null));
        }
        else if (source instanceof Map) {
            List keysToRemove = new LinkedList();
            for (Object key : ((Map) source).keySet()) {
                Object value = ((Map)source).get(key);
                if (value instanceof List) {
                    if (((List) value).size() == 0)
                        keysToRemove.add(key);
                    else {
                        compactJson(value);
                    }
                } else if (value instanceof Map) {
                    if (((Map) value).size() == 0) {
                        keysToRemove.add(key);
                    } else {
                        compactJson(value);
                    }
                } else if (value == null) {
                    keysToRemove.add(key);
                }
            }
            for(Object key: keysToRemove) {
                ((Map) source).remove(key);
            }
        }
        else {
            throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" );
        }

        return source;
    }

    /**
     * For a given non-null (json) object, save the valve in the nested path provided
     *
     * @param source the source json object
     * @param value the value to store
     * @param paths var args Object path to navigate down and store the object in
     * @return previously stored value if available, null otherwise
     */
    @SuppressWarnings( "unchecked" )
    public static  T store( Object source, T value, Object... paths ) {
        int destKeyIndex = paths.length - 1;
        if(destKeyIndex < 0) {
            throw new IllegalArgumentException( "No path information provided" );
        }
        if(source == null) {
            throw new NullPointerException( "source cannot be null" );
        }
        for ( int i = 0; i < destKeyIndex; i++ ) {
            Object currentPath = paths[i];
            Object nextPath = paths[i+1];
            source = getOrCreateNextObject( source, currentPath, nextPath );
        }
        Object path = paths[destKeyIndex];
        if(source instanceof Map && path instanceof String) {
            return cast( ( (Map) source ).put( path, value ) );
        }
        else if(source instanceof List && path instanceof Integer) {
            ensureListAvailability( (List) source, (int) path );
            return cast( ( (List) source ).set( (int) path, value ) );
        }
        else {
            throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" );
        }
    }

    /**
     * For a given non-null (json) object, removes and returns the value in the nested path provided
     *
     * Warning: changes array order, to maintain order, use store(source, null, path ...) instead
     *
     * @param source the source json object
     * @param paths var args Object path to navigate down and remove
     * @return existing value if available, null otherwise
     */
    @SuppressWarnings( "unchecked" )
    public static  T remove( Object source, Object... paths ) {
        int destKeyIndex = paths.length - 1;
        if(destKeyIndex < 0) {
            throw new IllegalArgumentException( "No path information provided" );
        }
        if(source == null) {
            throw new NullPointerException( "source cannot be null" );
        }
        for ( int i = 0; i < destKeyIndex; i++ ) {
            Object currentPath = paths[i];
            Object nextPath = paths[i+1];
            source = getOrCreateNextObject( source, currentPath, nextPath );
        }
        Object path = paths[destKeyIndex];
        if(source instanceof Map && path instanceof String) {
            return cast( ( (Map) source ).remove( path ) );
        }
        else if(source instanceof List && path instanceof Integer) {
            ensureListAvailability( (List) source, (int) path );
            return cast( ( (List) source ).remove( (int) path) );
        }
        else {
            throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" );
        }
    }

    @SuppressWarnings( "unchecked" )
    private static void ensureListAvailability( List source, int index ) {
        for ( int i = source.size(); i <= index; i++ ) {
            source.add( i, null );
        }
    }

    @SuppressWarnings( "unchecked" )
    private static Object getOrCreateNextObject( Object source, Object key, Object nextKey ) {
        Object value;
        if ( source instanceof Map && key instanceof String ) {
            if ( ( value = ( (Map) source ).get( key ) ) == null ) {
                Object newValue;
                if ( nextKey instanceof String ) {
                    newValue = new HashMap();
                }
                else if ( nextKey instanceof Integer ) {
                    newValue = new LinkedList();
                }
                else {
                    throw new UnsupportedOperationException( "Only String and Integer types are supported" );
                }
                ( (Map) source ).put( key, newValue );
                value = newValue;
            }
        }
        else if ( source instanceof List && key instanceof Integer ) {
            ensureListAvailability( ( (List) source ), (int) key );
            if ( ( value = ( (List) source ).get( (int) key ) ) == null ) {
                Object newValue;
                if ( nextKey instanceof String ) {
                    newValue = new HashMap();
                }
                else if ( nextKey instanceof Integer ) {
                    newValue = new LinkedList();
                }
                else {
                    throw new UnsupportedOperationException( "Only String and Integer types are supported" );
                }
                ( (List) source ).set( (int) key, newValue );
                value = newValue;
            }
        }
        else if(source == null || key == null) {
            throw new NullPointerException( "source and/or key cannot be null" );
        }
        else {
            throw new UnsupportedOperationException( "Only Map and List types are supported" );
        }

        if ( ( nextKey instanceof String && value instanceof Map ) || ( nextKey instanceof Integer && value instanceof List ) ) {
            return value;
        }
        else {
            throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy