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

com.bazaarvoice.jolt.modifier.function.Function 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.modifier.function;

import com.bazaarvoice.jolt.common.Optional;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Modifier supports a Function on RHS that accepts jolt path expressions as arguments and evaluates
 * them at runtime before calling it. Function always returns an Optional, and the value is written
 * only if the optional is not empty.
 *
 * function spec is defined by "key": "=functionName(args...)"
 *
 *
 * input:
 *      { "num": -1.0 }
 * spec:
 *      { "num": "=abs(@(1,&0))" }
 *      will call the stock function Math.abs() and will pass the matching value at "num"
 *
 * spec:
 *      { "num": "=abs" }
 *      an alternative shortcut will do the same thing
 *
 * output:
 *      { "num": 1.0 }
 *
 *
 *
 * input:
 *      { "value": -1.0 }
 *
 * spec:
 *      { "absValue": "=abs(@(1,value))" }
 *      will evaluate the jolt path expression @(1,value) and pass the output to stock function Math.abs()
 *
 * output:
 *      { "value": -1.0, "absValue": 1.0 }
 *
 *
 *
 * Currently defined stock functions are:
 *
 *      toLower     - returns toLower value of toString() value of first arg, rest is ignored
 *      toUpper     - returns toUpper value of toString() value of first arg, rest is ignored
 *      concat      - concatenate all given arguments' toString() values
 *
 *      min       - returns the min of all numbers provided in the arguments, non-numbers are ignored
 *      max       - returns the max of all numbers provided in the arguments, non-numbers are ignored
 *      abs         - returns the absolute value of first argument, rest is ignored
 *      toInteger   - returns the intValue() value of first argument if its numeric, rest is ignored
 *      toDouble    - returns the doubleValue() value of first argument if its numeric, rest is ignored
 *      toLong      - returns the longValue() value of first argument if its numeric, rest is ignored
 *
 * All of these functions returns Optional.EMPTY if unsuccessful, which results in a no-op when performing
 * the actual write in the json doc.
 *
 * i.e.
 * input:
 *      { "value1": "xyz" } --- note: string, not number
 *      { "value1": "1.0" } --- note: string, not number
 *
 * spec:
 *      { "value1": "=abs" } --- fails silently
 *      { "value2": "=abs" }
 *
 * output:
 *      { "value1": "xyz", "value2": "1" } --- note: "absValue": null is not inserted
 *
 *
 * This is work in progress, and probably will be changed in future releases. Hence it is marked for
 * removal as it'll eventually be moved to a different package as the Function feature is baked into
 * other transforms as well. In short this interface is not yet ready to be implemented outside jolt!
 *
 */

@Deprecated
public interface Function {

    Optional apply(Object... args);

    /**
     * Does nothing
     *
     * spec - "key": "=noop"
     *
     * will cause the key to remain unchanged
     */
    Function noop = new Function() {
        @Override
        public Optional apply( final Object... args ) {
            return Optional.empty();
        }
    };

    /**
     * Returns the first argument, null or otherwise
     *
     * spec - "key": [ "=isPresent", "otherValue" ]
     *
     * input - "key": null
     * output - "key": null
     *
     * input - "key": "value"
     * output - "key": "value"
     *
     * input - key is missing
     * output - "key": "otherValue"
     *
     */
    Function isPresent = new Function() {
        @Override
        public Optional apply( final Object... args ) {
            if (args.length == 0) {
                return Optional.empty();
            }
            return Optional.of( args[0] );
        }
    };

    /**
     * Returns the first argument if in not null
     *
     * spec - "key": ["=notNull", "otherValue" ]
     *
     * input - "key": null
     * output - "key": "otherValue"
     *
     * input - "key": "value"
     * output - "key": "value"
     *
     */
    Function notNull = new Function() {
        @Override
        public Optional apply( final Object... args ) {
            if (args.length == 0 || args[0] == null) {
                return Optional.empty();
            }
            return Optional.of( args[0] );
        }
    };

    /**
     * Returns the first argument if it is null
     *
     * spec - "key": ["=inNull", "otherValue" ]
     *
     * input - "key": null
     * output - "key": null
     *
     * input - "key": "value"
     * output - "key": "otherValue"
     *
     */
    Function isNull = new Function() {
        @Override
        public Optional apply( final Object... args ) {
            if (args.length == 0 || args[0] != null) {
                return Optional.empty();
            }
            return Optional.of( args[0] );
        }
    };

    /**
     * Abstract class that processes var-args and calls two abstract methods
     *
     * If its single list arg, or many args, calls applyList()
     * else calls applySingle()
     *
     * @param  type of return value
     */
    @SuppressWarnings( "unchecked" )
    abstract class BaseFunction implements Function {

        public final Optional apply( final Object... args ) {
            if(args.length == 0) {
                return Optional.empty();
            }
            else if(args.length == 1) {
                if(args[0] instanceof List ) {
                    if(((List) args[0]).isEmpty()) {
                        return Optional.empty();
                    }
                    else {
                        return applyList((List) args[0]);
                    }
                }
                else if( args[0] instanceof Object[] ) {
                    if(((Object[]) args[0]).length == 0) {
                        return Optional.empty();
                    }
                    else {
                        return applyList(Arrays.asList(((Object[]) args[0])));
                    }
                }
                else if(args[0] == null) {
                    return Optional.empty();
                }
                else {
                    return (Optional) applySingle( args[0] );
                }
            }
            else {
                return applyList( Arrays.asList( args ) );
            }
        }

        protected abstract Optional applyList( final List input );

        protected abstract Optional applySingle( final Object arg );
    }

    /**
     * Abstract class that provides rudimentary abstraction to quickly implement
     * a function that works on an single value input
     *
     * i.e. toUpperCase a string
     *
     * @param  type of return value
     */
    @SuppressWarnings( "unchecked" )
    abstract class SingleFunction extends BaseFunction {

        protected final Optional applyList( final List input ) {
            List ret = new ArrayList<>( input.size() );
            for(Object o: input) {
                Optional optional = applySingle( o );
                ret.add(optional.isPresent()?optional.get():o);
            }
            return Optional.of( ret );
        }

        protected abstract Optional applySingle( final Object arg );
    }

    /**
     * Abstract class that provides rudimentary abstraction to quickly implement
     * a function that works on an List of input
     *
     * i.e. find the max item from a list, etc.
     *
     */
    @SuppressWarnings( "unchecked" )
    abstract class ListFunction extends BaseFunction {

        protected abstract Optional applyList( final List argList );

        protected final Optional applySingle( final Object arg ) {
            return Optional.empty();
        }
    }

    /**
     * Abstract class that provides rudimentary abstraction to quickly implement
     * a function that classifies first arg as special input and rest as regular
     * input.
     *
     * @param  type of special argument
     * @param  type of return value
     */
    @SuppressWarnings( "unchecked" )
    abstract class ArgDrivenFunction implements Function {

        private final Class specialArgType;

        private ArgDrivenFunction() {
            /**
             * inspired from {@link com.google.common.reflect.TypeCapture#capture()}
             * copied, coz jolt-core is designed to have no dependency
             * modified, coz the instanceof check and subsequently throwing exception
             * is unnecessary as we already know this class has genericSuperClass of
             * Parametrized type. In worst case if an implementation does not specify
             * the generics, we fall back to Object.class, and that's ok.
             */
            Type superclass = getClass().getGenericSuperclass();
            if(superclass instanceof ParameterizedType) {
                specialArgType =  (Class) ((ParameterizedType) superclass).getActualTypeArguments()[0];
            }
            else {
                specialArgType =  (Class) Object.class;
            }
        }

        private Optional getSpecialArg( Object[] args) {
            if ( (args.length >= 2) && specialArgType.isInstance( args[0]) ) {
                SOURCE specialArg = (SOURCE) args[0];
                return Optional.of( specialArg );
            }
            return Optional.empty();
        }

        @Override
        public final Optional apply( Object... args ) {

            if(args.length == 1 && args[0] instanceof List) {
                args = ((List) args[0]).toArray();
            }

            Optional specialArgOptional = getSpecialArg( args );
            if ( specialArgOptional.isPresent() ) {
                SOURCE specialArg = specialArgOptional.get();
                if ( args.length == 2) {
                    if(args[1] instanceof List) {
                        return (Optional) applyList( specialArg, (List) args[1] );
                    }
                    else {
                        return (Optional) applySingle( specialArg, args[1] );
                    }
                }
                else {
                    List input = Arrays.asList( Arrays.copyOfRange(args, 1, args.length) );
                    return applyList( specialArg, input );
                }
            }
            else {
                return Optional.empty();
            }
        }

        protected abstract Optional applyList( SOURCE specialArg, List args );

        protected abstract Optional applySingle( SOURCE specialArg, Object arg );
    }

    /**
     * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly
     * implement a function that works on a single input
     *
     * i.e. increment(1, value)
     *
     * @param  type of special argument
     * @param  type of return value
     */
    @SuppressWarnings( "unchecked" )
    abstract class ArgDrivenSingleFunction extends ArgDrivenFunction {

        protected final Optional applyList( S specialArg, List input ) {
            List ret = new ArrayList<>( input.size() );
            for(Object o: input) {
                Optional optional = applySingle( specialArg, o );
                ret.add(optional.isPresent()?optional.get():o);
            }
            return (Optional) Optional.of( ret );
        }

        protected abstract Optional applySingle( S specialArg, Object arg );
    }

    /**
     * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly
     * implement a function that works on an input list|array
     *
     * i.e. join('-', ...)
     *
     * @param  type of special argument
     */
    @SuppressWarnings( "unchecked" )
    abstract class ArgDrivenListFunction extends ArgDrivenFunction {

        protected abstract Optional applyList( S specialArg, List args );

        protected final Optional applySingle( S specialArg, Object arg ) {
            return Optional.empty();
        }
    }

    /**
     * squashNull is a special kind of null processing,the input is always a list or map as a singleton
     *
     * @param  type of return value
     */
    abstract class SquashFunction implements Function {

        public final Optional apply( final Object... args ) {
            if(args.length == 0) {
                return Optional.empty();
            }
            else if(args.length == 1) {
                if(args[0] instanceof List ) {
                    if(((List) args[0]).isEmpty()) {
                        return Optional.empty();
                    }
                    else {
                        return (Optional)applySingle((List) args[0]);
                    }
                }
                else if( args[0] instanceof Object[] ) {
                    if(((Object[]) args[0]).length == 0) {
                        return Optional.empty();
                    }
                    else {
                        return (Optional)applySingle(Arrays.asList(((Object[]) args[0])));
                    }
                }
                else if(args[0] == null) {
                    return Optional.empty();
                }
                else {
                    return (Optional) applySingle( args[0] );
                }
            }
            else {
                return (Optional)applySingle( Arrays.asList( args ) );
            }
        }

        protected abstract Optional applySingle( final Object arg );
    }

}