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

com.bazaarvoice.jolt.Chainr 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;

import com.bazaarvoice.jolt.chainr.ChainrBuilder;
import com.bazaarvoice.jolt.chainr.instantiator.ChainrInstantiator;
import com.bazaarvoice.jolt.exception.SpecException;
import com.bazaarvoice.jolt.exception.TransformException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Chainr is the JOLT mechanism for chaining {@link JoltTransform}s together. Any of the built-in JOLT
 * transform types can be called directly from Chainr. Any custom-written Java transforms
 * can be adapted in by implementing the {@link Transform} or {@link SpecDriven} interfaces.
 *
 * A Chainr spec should be an array of objects in order that look like this:
 *
 * [
 *     {
 *         "operation": "[operation-name]",
 *         // stuff that the specific transform needs go here
 *     },
 *     ...
 * ]
 *
 * Each operation is called in the order that it is specified within the array. The original
 * input to Chainr is passed into the first operation, with its output passed into the next,
 * and so on. The output of the final operation is returned from Chainr.
 *
 * Currently, [operation-name] can be any of the following:
 *
 * - shift: ({@link Shiftr}) a tool for moving parts of an input JSON document to a new output document
 * - default: ({@link Defaultr}) a tool for applying default values to the provided JSON document
 * - remove: ({@link Removr}) a tool for removing specific values from the provided JSON document
 * - sort: ({@link Sortr}) sort the JSON document
 * - java: passes control to whatever Java class you specify as long as it implements the {@link Transform} interface
 *
 * Shift, default, and remove operation all require a "spec", while sort does not.
 *
 * [
 *     {
 *         "operation": "shift",
 *         "spec" : { // shiftr spec }
 *     },
 *     {
 *         "operation": "sort"  // sort does not need a spec
 *     },
 *     ...
 * ]
 *
 * Custom Java classes that implement {@link Transform} and/or {@link SpecDriven} can be loaded by specifying the full
 *  className to load. Additionally, if upon reflection of the class we see that it is an instance of a
 *  {@link SpecDriven}, then we will construct it with a the supplied "spec" object.
 *
 * [
 *     {
 *         "operation": "com.bazaarvoice.tuna.CustomTransform",
 *
 *         "spec" : { ... } // optional spec to use to construct a custom {@link Transform} if it has the {@link SpecDriven} marker interface.
 *     },
 *     ...
 * ]
 */
public class Chainr implements Transform, ContextualTransform {

    // The list of Transforms we will march through on every call to chainr.
    // Note this will contain actual ContextualTransforms and adapted Transforms.
    private final List transformsList;

    // The list of actual ContextualTransforms, for clients that specifically care.
    private final List actualContextualTransforms;

    public static Chainr fromSpec( Object input ) {
        return new ChainrBuilder( input ).build();
    }

    public static Chainr fromSpec( Object input, ChainrInstantiator instantiator ) {
        return new ChainrBuilder( input ).loader( instantiator ).build();
    }

    /**
     * Adapt "normal" Transforms to look like ContextualTransforms, so that
     *  Chainr can just maintain a single list of "JoltTransforms" to run.
     */
    private static class ContextualTransformAdapter implements ContextualTransform {

        private final Transform transform;

        private ContextualTransformAdapter( Transform transform ) {
            this.transform = transform;
        }

        @Override
        public Object transform( Object input, Map context ) {
            return transform.transform( input );
        }
    }

    public Chainr( List joltTransforms ) {

        if ( joltTransforms == null ) {
            throw new IllegalArgumentException( "Chainr requires a list of JoltTransforms." );
        }

        transformsList = new ArrayList( joltTransforms.size() );
        List realContextualTransforms = new LinkedList();

        for ( JoltTransform joltTransform : joltTransforms ) {

            // Do one pass of "instanceof" checks at construction time, rather than repeatedly at "runtime".
            boolean isTransform = joltTransform instanceof Transform;
            boolean isContextual = joltTransform instanceof ContextualTransform;

            if ( isContextual && isTransform ) {
                throw new SpecException( "JOLT Chainr - JoltTransform className:" + joltTransform.getClass().getCanonicalName() +
                        " implements both Transform and ContextualTransform, should only implement one of those interfaces." );
            }
            if ( ! isContextual && ! isTransform ) {
                throw new SpecException( "JOLT Chainr - Transform className:" + joltTransform.getClass().getCanonicalName() +
                        " should implement Transform or ContextualTransform." );
            }

            // We are optimizing given the assumption that Chainr objects will be built and then reused many times.
            // We want to have a single list of "transforms" that we can just blindly march through.
            // In order to accomplish this, we adapt Transforms to look like ContextualTransforms and just maintain
            //  a list of type ContextualTransform.
            if ( isContextual ) {
                transformsList.add( (ContextualTransform) joltTransform );
                realContextualTransforms.add( (ContextualTransform) joltTransform );
            }
            else
            {
                transformsList.add( new ContextualTransformAdapter( (Transform) joltTransform ) );
            }
        }

        actualContextualTransforms = Collections.unmodifiableList( realContextualTransforms );
    }

    /**
     * Runs a series of Transforms on the input, piping the inputs and outputs of the Transforms together.
     *
     * Chainr instances are meant to be immutable once they are created so that they can be
     * used many times.
     *
     * The notion of passing "context" to the transforms allows chainr instances to be
     * reused, even in situations were you need to slightly vary.
     *
     * @param input a JSON (Jackson-parsed) maps-of-maps object to transform
     * @param context optional tweaks that the consumer of the transform would like
     * @return an object representing the JSON resulting from the transform
     * @throws com.bazaarvoice.jolt.exception.TransformException if the specification is malformed, an operation is not
     *                       found, or if one of the specified transforms throws an exception.
     */
    @Override
    public Object transform( Object input, Map context ) {
        return doTransform( transformsList, input, context );
    }

    @Override
    public Object transform( Object input ) {
        return doTransform( transformsList, input, null );
    }

    /**
     * Have Chainr run a subset of the transforms in it's spec.
     *
     * Useful for testing and debugging.
     *
     * @param input the input data to transform
     * @param to transform from the chainrSpec to end with: 0 based index exclusive
     */
    public Object transform( int to, Object input ) {
        return transform( 0, to, input, null );
    }

    /**
     * Useful for testing and debugging.
     *
     * @param input the input data to transform
     * @param to transform from the chainrSpec to end with: 0 based index exclusive
     * @param context optional tweaks that the consumer of the transform would like
     */
    public Object transform( int to, Object input, Map context ) {
        return transform( 0, to, input, context );
    }

    /**
     * Useful for testing and debugging.
     *
     * @param input the input data to transform
     * @param from transform from the chainrSpec to start with: 0 based index
     * @param to transform from the chainrSpec to end with: 0 based index exclusive
     */
    public Object transform( int from, int to, Object input ) {
        return transform( from, to, input, null );
    }

    /**
     * Have Chainr run a subset of the transforms in it's spec.
     *
     * Useful for testing and debugging.
     *
     * @param input the input data to transform
     * @param from transform from the chainrSpec to start with: 0 based index
     * @param to transform from the chainrSpec to end with: 0 based index exclusive
     * @param context optional tweaks that the consumer of the transform would like
     */
    public Object transform( int from, int to, Object input, Map context ) {

        if ( from < 0 || to > transformsList.size() || to <= from ) {
            throw new TransformException( "JOLT Chainr : invalid from and to parameters : from=" + from + " to=" + to );
        }

        return doTransform( transformsList.subList( from, to ), input, context );
    }

    private static Object doTransform( List transforms, Object input, Map context ) {

        Object intermediate = input;
        for ( ContextualTransform transform : transforms ) {
            intermediate = transform.transform( intermediate, context );
        }

        return intermediate;
    }

    /**
     * @return true if this Chainr instance has any ContextualTransforms
     */
    public boolean hasContextualTransforms() {
        return !actualContextualTransforms.isEmpty();
    }

    /**
     * This method allows Chainr clients to examine the ContextualTransforms
     * in this Chainr instance.  This may be helpful when building the "context".
     *
     * @return List of ContextualTransforms used by this Chainr instance
     */
    public List getContextualTransforms() {
        return actualContextualTransforms;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy