DataType.java

/*
 * 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;

import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.common.tree.WalkedPath;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * From the spec we need to guess the DataType of the incoming input
 *
 * This is useful for,
 * a) in cases where the spec suggested a list but input was map
 *    and vice versa, where we can just skip processing instead of
 *    throwing random array/map errors
 * b) in case where the input is actually null and we need to create
 *    appropriate data structure and then apply spec logic
 *
 * Note: By design jolt does not stop processing on bad input data
 */
public abstract class DataType {

    private static final RUNTIME runtimeInstance = new RUNTIME();
    private static final MAP mapInstance = new MAP();

    public static DataType determineDataType( int confirmedArrayAtIndex, int confirmedMapAtIndex, int maxExplicitIndex ) {
        // based on provided flags, set appropriate dataType
        if ( confirmedArrayAtIndex > -1 ) {
            return new LIST( maxExplicitIndex );
        }
        else if ( confirmedMapAtIndex > -1 ) {
            return mapInstance;
        }
        // only a single "*" key was defined in spec. We need to get dataType at runtime from input
        else {
            return runtimeInstance;
        }
    }

    /**
     * List type that records maxIndex from spec, and uses that to expand a source (list) properly
     */
    public static final class LIST extends DataType {
        private final int maxIndexFromSpec;

        private LIST( int maxIndexFromSpec ) {
            this.maxIndexFromSpec = maxIndexFromSpec;
        }

        @Override
        protected Object createValue() {
            return new ArrayList<>();
        }

        @Override
        @SuppressWarnings( "unchecked" )
        public Integer expand( Object input ) {
            List source = (List) input;
            int reqIndex = maxIndexFromSpec;
            int currLastIndex = source.size() - 1;
            int origSize = currLastIndex + 1;
            if ( reqIndex >= source.size() ) {
                while ( currLastIndex++ < reqIndex ) {
                    source.add( null );
                }
            }
            return origSize;
        }

        @Override
        public boolean isCompatible( final Object input ) {
            return input == null || input instanceof List;
        }
    }

    /**
     * MAP type class
     */
    public static final class MAP extends DataType {
        @Override
        protected Object createValue() {
            return new LinkedHashMap<>();
        }

        @Override
        public boolean isCompatible( final Object input ) {
            return input == null || input instanceof Map;
        }
    }

    /**
     * Runtime type
     */
    public static final class RUNTIME extends DataType {
        @Override
        public boolean isCompatible( final Object input ) {
            return input != null;
        }

        @Override
        protected Object createValue() {
            throw new RuntimeException( "Cannot create for RUNTIME Type" );
        }
    }

    /**
     * Determines if an input is compatible with current DataType
     */
    public abstract boolean isCompatible(Object input);

    /**
     * MAP and LIST types overrides this method to return appropriate new map or list
     */
    protected abstract Object createValue();

    /**
     * LIST overrides this method to expand the source (list) such that in can support
     * an index specified in spec that is outside the range input list, returns original size
     * of the input
     */
    public Integer expand( Object source ) {
        throw new RuntimeException( "Expand not supported in " + this.getClass().getSimpleName() + " Type" );
    }

    /**
     * Creates an empty map/list, as required by spec, in the parent map/list at given key/index
     *
     * @param keyOrIndex of the parent object to create
     * @param walkedPath containing the parent object
     * @param opMode     to determine if this write operation is allowed
     * @return newly created object
     */
    @SuppressWarnings( "unchecked" )
    public Object create( String keyOrIndex, WalkedPath walkedPath, OpMode opMode ) {
        Object parent = walkedPath.lastElement().getTreeRef();
        Optional<Integer> origSizeOptional = walkedPath.lastElement().getOrigSize();
        int index = -1;
        try {
            index = Integer.parseInt( keyOrIndex );
        }
        catch ( Exception ignored ) {
        }
        Object value = null;
        if ( parent instanceof Map && opMode.isApplicable( (Map) parent, keyOrIndex ) ) {
            value = createValue();
            ( (Map) parent ).put( keyOrIndex, value );
        }
        else if ( parent instanceof List && opMode.isApplicable( (List) parent, index, origSizeOptional.get() ) ) {
            value = createValue();
            ( (List) parent ).set( index, value );
        }
        return value;
    }
}